Oinone开发实践-业务实现多租户方案

总体方案

具体实现方式

1、业务上定义两个基础抽象模型包含租户字段

定义包含ID的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。

@Model.model(XXIdModel.MODEL_MODEL)
@Model.Advanced(type = ModelTypeEnum.ABSTRACT)
@Model(displayName = "带公司CODE的基础ID抽象模型", summary = "待公司Code的Id模型")
public abstract class XXIdModel extends IdModel {
    public static final String MODEL_MODEL = "demo.biz.XXIdModel";

    @Field.String
    @Field(displayName = "所属公司编码", invisible = true, index = true)
    private String companyCode;
}

定义包含Code的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。

@Model.model(XXCodeModel.MODEL_MODEL)
@Model.Advanced(type = ModelTypeEnum.ABSTRACT)
@Model(displayName = "带公司CODE的基础Code抽象模型", summary = "带公司CODE的Code模型")
public abstract class XXCodeModel extends CodeModel {
    public static final String MODEL_MODEL = "demo.biz.XXCodeModel";

    @Field.String
    @Field(displayName = "所属公司编码", invisible = true, index = true)
    private String companyCode;
}

2、业务模块的模型需租户隔离的都是继承上面这两个模型;

@Model.model(PetPetCompany.MODEL_MODEL)
@Model(displayName = "宠物公司", labelFields = "name")
public class PetPetCompany extends AbstractCompanyCodeModel {

    public static final String MODEL_MODEL = "demo.PetPetCompany";

    @Field.String
    @Field(displayName = "名称")
    private String name;

    @Field.Text
    @Field(displayName = "简介")
    private String introduction;
}

3、自定义扩展Session,Session中设置租户信息

每次请求多把登录用户所属公司编码(companyCode)放到Session中;
Session扩展参考:https://doc.oinone.top/oio4/9295.html

4、定义拦截器Interceptor进行数据隔离

数据创建和查询通过拦截器把Session中的中的公司编码(companyCode)设置到隔离字段中;拦截器的java示例代码参考:

package pro.shushi.pamirs.demo.core.interceptor;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor;
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.values.ValuesStatement;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.core.session.DemoSession;
import pro.shushi.pamirs.framework.connectors.data.mapper.context.MapperContext;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;

import java.util.Map;
import java.util.Properties;

// https://www.jianshu.com/p/8ad3b4d6bd43
@Slf4j
@SuppressWarnings("unused")
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
@Order(99)
@ConditionalOnProperty(value = "pamirs.demo.isolation.enable", havingValue = "true")
public class IsolationCheckInterceptor implements Interceptor {

    private static final String[] BOUND_SQL_CLONE_FIELDS = new String[]{"additionalParameters", "metaParameters"};

    @Autowired
    private DemoIsolationConfiguration demoIsolationConfiguration;

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        if (SqlCommandType.FLUSH == ms.getSqlCommandType() || SqlCommandType.UNKNOWN == ms.getSqlCommandType()) {
            return invocation.proceed();
        }
        // sql type: UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
        String sqlCommandType = ms.getSqlCommandType().toString();
        // update 直接返回
        if ("UPDATE".equals(sqlCommandType)) {
            return invocation.proceed();
        }

        Object param = args[1];
        if (param instanceof Map) {
            Map map = (Map) param;
            // 获取配置信息
            String model = MapperContext.model(map);
            if (StringUtils.isBlank(model)) {
                return invocation.proceed();
            }

            if (!demoIsolationConfiguration.needIsolation(model)) {
                return invocation.proceed();
            }
            BoundSql boundSql = ms.getBoundSql(param);
            String sql = boundSql.getSql();
            // 通过jsqlparser解析SQL,此处的statement是封装过后的Insert/Update/Query等SQL语句
            Statement statement = CCJSqlParserUtil.parse(sql);

            switch (sqlCommandType) {
                case "INSERT":
                    Statement insert = prepareInsertSql(statement);
                    BoundSql insertBoundSql = new BoundSql(ms.getConfiguration(), insert.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
                    cloneBoundSqlParameters(boundSql, insertBoundSql);
                    MappedStatement insertMs = buildMappedStatement(ms, new BoundSqlSqlSource(insertBoundSql));
                    // 更新 MappedStatement 对象
                    args[0] = insertMs;
                    break;
                case "SELECT":
                    Statement select = prepareSelectSql(statement);
                    BoundSql selectBoundSql = new BoundSql(ms.getConfiguration(), select.toString(), boundSql.getParameterMappings(), boundSql.getParameterObject());
                    cloneBoundSqlParameters(boundSql, selectBoundSql);
                    MappedStatement selectMs = buildMappedStatement(ms, new BoundSqlSqlSource(selectBoundSql));
                    // 更新 MappedStatement 对象
                    args[0] = selectMs;
                    break;
                default:
                    break;
            }
        }

        return invocation.proceed();
    }

    private void cloneBoundSqlParameters(BoundSql boundSql, BoundSql targetBoundSql) {
        MetaObject boundSqlObject = SystemMetaObject.forObject(boundSql);
        MetaObject targetBoundSqlObject = SystemMetaObject.forObject(targetBoundSql);
        for (String field : BOUND_SQL_CLONE_FIELDS) {
            targetBoundSqlObject.setValue(field, boundSqlObject.getValue(field));
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // to do nothing
    }

    private MappedStatement buildMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(ms.getKeyProperties()[0]);
        }
        //禁止用缓存(重要)
        builder.flushCacheRequired(false);
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.cache(ms.getCache());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    private static class BoundSqlSqlSource implements SqlSource {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

    private Statement prepareInsertSql(Statement statement) {
        Insert insert = (Insert) statement;

        boolean isContainsIsolationColumn = false;
        int createDateColumnIndex = 0;
        for (int i = 0; i < insert.getColumns().size(); i++) {
            Column column = insert.getColumns().get(i);
            if (clearQuote(column.getColumnName()).equals(demoIsolationConfiguration.getColumn())) {
                // sql中包含了设置的列名,则只需要设置值
                isContainsIsolationColumn = true;
                createDateColumnIndex = i;
                break;
            }
        }

        if (!isContainsIsolationColumn) {
            intoValue("" + demoIsolationConfiguration.getColumn() + "", DemoSession.getCompany().getCode(), insert);
        } else {
            intoValueWithIndex(createDateColumnIndex, DemoSession.getCompany().getCode(), insert);
        }

        log.debug("intercept insert sql is : {}", insert);
        return insert;
    }

    private Statement prepareSelectSql(Statement statement) throws JSQLParserException {
        Select select = (Select) statement;
        PlainSelect plain = (PlainSelect) select.getSelectBody();
        FromItem fromItem = plain.getFromItem();

        StringBuffer whereSql = new StringBuffer();
        //增加sql语句的逻辑部分处理
        if (fromItem.getAlias() != null) {
            whereSql.append(fromItem.getAlias().getName()).append("." + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        } else {
            whereSql.append("" + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        }
        Expression where = plain.getWhere();
        if (where == null) {
            if (whereSql.length() > 0) {
                Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
                Expression whereExpression = (Expression) expression;
                plain.setWhere(whereExpression);
            }
        } else {
            if (whereSql.length() > 0) {
                //where条件之前存在,需要重新进行拼接
                whereSql.append(" and ( " + where.toString() + " )");
            } else {
                //新增片段不存在,使用之前的sql
                whereSql.append(where.toString());
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            plain.setWhere(expression);
        }

        return select;
    }

    private Statement prepareUpdateSql(Statement statement) throws JSQLParserException {
        Update update = (Update) statement;
        PlainSelect plain = (PlainSelect) update.getSelect().getSelectBody();
        FromItem fromItem = plain.getFromItem();

        StringBuffer whereSql = new StringBuffer();
        //增加sql语句的逻辑部分处理
        if (fromItem.getAlias() != null) {
            whereSql.append(fromItem.getAlias().getName()).append("." + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        } else {
            whereSql.append("" + demoIsolationConfiguration.getColumn() + " = ").append("'").append(DemoSession.getCompany().getCode()).append("'");
        }
        Expression where = plain.getWhere();
        if (where == null) {
            if (whereSql.length() > 0) {
                Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
                Expression whereExpression = expression;
                plain.setWhere(whereExpression);
            }
        } else {
            if (whereSql.length() > 0) {
                //where条件之前存在,需要重新进行拼接
                whereSql.append(" and ( " + where + " )");
            } else {
                //新增片段不存在,使用之前的sql
                whereSql.append(where);
            }
            Expression expression = CCJSqlParserUtil.parseCondExpression(whereSql.toString());
            plain.setWhere(expression);
        }

        return update;
    }

    /**
     * insert sql update column value
     *
     * @param index
     * @param columnValue
     * @param insert
     */
    private void intoValueWithIndex(final int index, final Object columnValue, Insert insert) {
        // 通过visitor设置对应的值
        if (insert.getItemsList() == null) {
            insert.getSelect().getSelectBody().accept(new PlainSelectVisitor(index, columnValue));
        } else {
            insert.getItemsList().accept(new ItemsListVisitor() {
                @Override
                public void visit(SubSelect subSelect) {
                    throw new UnsupportedOperationException("Not supported yet.");
                }

                @Override
                public void visit(NamedExpressionList namedExpressionList) {

                }

                @Override
                public void visit(ExpressionList expressionList) {
                    if (columnValue instanceof String) {
                        expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                    } else if (columnValue instanceof Long) {
                        expressionList.getExpressions().set(index, new LongValue((Long) columnValue));
                    } else {
                        // if you need to add other type data, add more if branch
                        expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                    }
                }

                @Override
                public void visit(MultiExpressionList multiExpressionList) {
                    for (ExpressionList expressionList : multiExpressionList.getExprList()) {
                        if (columnValue instanceof String) {
                            expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                        } else if (columnValue instanceof Long) {
                            expressionList.getExpressions().set(index, new LongValue((Long) columnValue));
                        } else {
                            // if you need to add other type data, add more if branch
                            expressionList.getExpressions().set(index, new StringValue((String) columnValue));
                        }
                    }
                }
            });
        }
    }

    /**
     * insert sql add column
     *
     * @param columnName
     * @param columnValue
     * @param insert
     */
    private void intoValue(String columnName, final Object columnValue, Insert insert) {
        // 添加列
        insert.getColumns().add(new Column(columnName));
        // 通过visitor设置对应的值
        if (insert.getItemsList() == null) {
            insert.getSelect().getSelectBody().accept(new PlainSelectVisitor(-1, columnValue));
        } else {
            insert.getItemsList().accept(new ItemsListVisitor() {
                @Override
                public void visit(SubSelect subSelect) {
                    throw new UnsupportedOperationException("Not supported yet.");
                }

                @Override
                public void visit(ExpressionList expressionList) {
                    // 这里表示添加列时。列值在数据库中的数据类型, 目前只用到了Long和String,需要的自行扩展
                    if (columnValue instanceof String) {
                        expressionList.getExpressions().add(new StringValue((String) columnValue));
                    } else if (columnValue instanceof Long) {
                        expressionList.getExpressions().add(new LongValue((Long) columnValue));
                    } else {
                        // if you need to add other type data, add more if branch
                        expressionList.getExpressions().add(new StringValue((String) columnValue));
                    }
                }

                @Override
                public void visit(NamedExpressionList namedExpressionList) {
                }

                @Override
                public void visit(MultiExpressionList multiExpressionList) {
                    for (ExpressionList expressionList : multiExpressionList.getExprList()) {
                        if (columnValue instanceof String) {
                            expressionList.getExpressions().add(new StringValue((String) columnValue));
                        } else if (columnValue instanceof Long) {
                            expressionList.getExpressions().add(new LongValue((Long) columnValue));
                        } else {
                            // if you need to add other type data, add more if branch
                            expressionList.getExpressions().add(new StringValue((String) columnValue));
                        }
                    }
                }
            });
        }
    }

    /**
     * 支持INSERT INTO SELECT 语句
     */
    private class PlainSelectVisitor implements SelectVisitor {
        int index;
        Object columnValue;

        public PlainSelectVisitor(int index, Object columnValue) {
            this.index = index;
            this.columnValue = columnValue;
        }

        @Override
        public void visit(PlainSelect plainSelect) {
            if (index != -1) {
                if (columnValue instanceof String) {
                    plainSelect.getSelectItems().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    plainSelect.getSelectItems().set(index, new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    plainSelect.getSelectItems().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            } else {
                if (columnValue instanceof String) {
                    plainSelect.getSelectItems().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    plainSelect.getSelectItems().add(new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    plainSelect.getSelectItems().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            }
        }

        @Override
        public void visit(SetOperationList setOperationList) {
            throw new UnsupportedOperationException("Not supported yet.");
        }

        @Override
        public void visit(WithItem withItem) {
            if (index != -1) {
                if (columnValue instanceof String) {
                    withItem.getWithItemList().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    withItem.getWithItemList().set(index, new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    withItem.getWithItemList().set(index, new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            } else {
                if (columnValue instanceof String) {
                    withItem.getWithItemList().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                } else if (columnValue instanceof Long) {
                    withItem.getWithItemList().add(new SelectExpressionItem(new LongValue((Long) columnValue)));
                } else {
                    // if you need to add other type data, add more if branch
                    withItem.getWithItemList().add(new SelectExpressionItem(new StringValue((String) columnValue)));
                }
            }
        }

        @Override
        public void visit(ValuesStatement valuesStatement) {

        }
    }

    /**
     * 去除''号
     *
     * @param value
     * @return {@link String}
     */
    private String clearQuote(String value) {
        if (value.startsWith("") && value.endsWith("")) {
            value = value.substring(1, value.length() - 1);
        }
        return value;
    }

}

5、租户配置信息代码实现

package pro.shushi.pamirs.demo.core.interceptor;

import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import pro.shushi.pamirs.meta.annotation.fun.Data;
import pro.shushi.pamirs.meta.api.dto.config.ModelConfig;
import pro.shushi.pamirs.meta.api.dto.config.ModelFieldConfig;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.common.constants.ModuleConstants;

import java.util.List;
import java.util.Optional;

@Data
@Configuration
@ConfigurationProperties(prefix = "pamirs.demo.isolation")
public class DemoIsolationConfiguration {

    private Boolean enable = Boolean.FALSE;
    /**隔离字段对应的数据表column*/
    private String column;
    /**隔离字段对应的模型field*/
    private String field;
    /**即使包含隔离字段field,也不需要隔离的模型列表。 实际项目中也可采用白名单方式*/
    private List ignoreModels = Lists.newArrayList("business.PamirsEmployee", "demo.PetEmployee");

    public boolean needIsolation(String model) {
        // 1、在忽略列表中的不需要进行隔离
        if (matchesIgnoreModel(model)) {
            return false;
        }

        // 2、模块为空或者为base模块的不需要进行隔离
        String module = Optional.ofNullable(PamirsSession.getContext())
                .map(v -> v.getModelConfig(model)).map(ModelConfig::getModule).orElse(null);
        if (StringUtils.isBlank(module) || module.equals(ModuleConstants.MODULE_BASE)) {
            return false;
        }

        // 3、超级管理员进入不需要进行隔离(根据实际情况确定是否开启)
        /**
        if (PamirsSession.getAdminTag()==null || PamirsSession.getAdminTag()) {
            return false;
        }**/

        // 4、模型中无隔离字段的不需要进行隔离
        ModelFieldConfig modelField = PamirsSession.getContext().getModelField(model, field);
        if (modelField == null) {
            return false;
        }

        return true;
    }

    private boolean matchesIgnoreModel(String model) {
        if (CollectionUtils.isEmpty(ignoreModels)) {
            return false;
        }
        if (ignoreModels.contains(model)) {
            return true;
        }
        return false;
    }
}

6、对应的yml文件配置示例

  demo:
    isolation:
      enable: true
      field:  companyCode
      column: company_code
      # ignoreModels:

7、本文中的示例代码附件

拦截器实现租户隔离示例 interceptor
Session扩展示例 Session扩展示例

Oinone社区 作者:望闲原创文章,如若转载,请注明出处:https://doc.oinone.top/backend/6525.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
望闲的头像望闲数式管理员
上一篇 2024年4月3日 下午4:39
下一篇 2024年4月7日 下午7:48

相关推荐

  • Oinone项目引入Nacos作为注册中心

    Oinone项目引入Nacos作为注册中心 Oinone项目的默认dubbo注册中心为zk, 实际项目中有可能要求用Nacos作注册中心。 Oinone默认引入的nacos-client-1.4.1,低版本不支持认证配置;该客户端版本支持Nacos服务1.x的和2.x的版本 一、项目中增加依赖 项目主pom引入依赖。 <dependency> &…

    2024年2月28日
    27800
  • 【达梦】后端部署使用达梦数据库

    达梦数据库配置 驱动配置 达梦数据库的服务端版本和驱动版本需要匹配,建议使用服务端安装时提供的jdbc驱动,不要使用官方maven仓库中的驱动。 报错 表 xx 中不能同时包含聚集 KEY 和大字段,建表的时候就指定非聚集主键。SELECT * FROM V$DM_INI WHERE PARA_NAME = ‘PK_WITH_CLUSTER’;SP_SET_…

    后端 2023年11月1日
    13200
  • 框架之MessageHub(信息提示)

    框架之信息概述 后端除了可以返回错误信息以外,还可以返回调试、告警、成功、信息等级别的信息给前端。但是默认情况下前端只提示错误信息,可以通过前端的统一配置放开提示级别,有点类似后端的日志级别。 框架之MessageHub 在oinone平台中,我们怎么做到友好的错误提示呢?接下来介绍我们的MessageHub,它为自定义错误提示提供无限的可能。 何时使用 错…

    2024年5月14日
    26900
  • 函数之触发与定时配置和示例

    异步任务总体介绍 函数的触发和定时在很多场景中会用到,也是一个oinone的基础能力。比如我们的流程产品中在定义流程触发时就会让用户选择模型触发还是时间触发,就是用到了函数的触发与定时能力。 触发任务TriggerTaskAction 触发任务的创建,使用sql-record模块监听mysql的binlog事件,通过rocketmq发送变更数据消息,收到MQ…

    2024年5月25日
    30000
  • Oinone连接外部数据源方案

    场景描述 在实际业务场景中,有是有这样的需求:链接外部数据进行数据的获取;通常的做法:1、【推荐】通过集成平台的数据连接器,链接外部数据源进行数据操作;2、项目代码中链接数据源,即通过程序代码操作外部数据源的数据; 本篇文章只介绍通过程序代码操作外部数据源的方式. 整体方案 Oinone管理外部数据源,即yml中配置外部数据源; 后端通过Mapper的方式进…

    2024年5月17日
    20400

发表回复

登录后才能评论