如何解决使用mybatis-plus提供的多租户插件出现Column ‘tenant_id‘ specified twice问题

Posted linyb极客之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何解决使用mybatis-plus提供的多租户插件出现Column ‘tenant_id‘ specified twice问题相关的知识,希望对你有一定的参考价值。

前言

本文案例来源于业务开发部门进行多租户开发时发生的案例。用过mybatis-plus多租户插件的朋友,可能会知道,该插件的租户id值基本都是从上下文得来,这个上下文可以是cookie、session、threadlocal等。据业务部门反馈,在某次插入时,他们发现获取不到租户id值,于是他们在他们的代码层面上做了这么一层操作,在保存的时候,设置租户id。保存的时候,很成功的出现了Column \'tenant_id\' specified twice

问题来源

在mybatis-plus 3.4版本之前,mybatis-plus进行多租户插入时是不会对已经存在的tenant_id进行过滤的,这就导致出现Column \'tenant_id\' specified twice问题。其3.4版本之前多租户sql解析器处理insert语句源码如下

  @Override
    public void processInsert(Insert insert) {
        if (tenantHandler.doTableFilter(insert.getTable().getName())) {
            // 过滤退出执行
            return;
        }
        insert.getColumns().add(new Column(tenantHandler.getTenantIdColumn()));
        if (insert.getSelect() != null) {
            processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
        } else if (insert.getItemsList() != null) {
            // fixed github pull/295
            ItemsList itemsList = insert.getItemsList();
            if (itemsList instanceof MultiExpressionList) {
                ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(tenantHandler.getTenantId(false)));
            } else {
                ((ExpressionList) insert.getItemsList()).getExpressions().add(tenantHandler.getTenantId(false));
            }
        } else {
            throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
        }
    }

问题解决方案

1、方案一:在业务代码插入时,实体不要设置租户id值,统一由多租户插件进行设值

2、方案二:升级mybatis-plus版本为3.4.1或者之后的版本

不过此时的多租户插件的写法就不要按之前那种方式写,虽然之前写法3.4.1也兼容,不过官方已经打了@Deprecated标注,说明官方已经不推荐之前那种写法了,因此采用官方最新提供租户插件拦截器。其示例代码如下

  /**
     * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                return new LongValue(1);
            }

            // 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
            @Override
            public boolean ignoreTable(String tableName) {
                return !"user".equalsIgnoreCase(tableName);
            }
        }));
        // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
        // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
//        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.setUseDeprecatedExecutor(false);
    }

TenantLineInnerInterceptor这个拦截器的包在com.baomidou.mybatisplus.extension.plugins.inner这个包下

3、方案三:如果是使用mybatis-plus3.4.1之前的版本,可以通过自定义一个TenantSqlParser解析器并重写processInsert方法,其核心代码如下
  */
    @Override
    public void processInsert(Insert insert) {
        if (getTenantHandler().doTableFilter(insert.getTable().getName())) {
            // 过滤退出执行
            return;
        }
        if (isAleadyExistTenantColumn(insert)) {
            return;
        }
        insert.getColumns().add(new Column(getTenantHandler().getTenantIdColumn()));
        if (insert.getSelect() != null) {
            processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
        } else if (insert.getItemsList() != null) {
            // fixed github pull/295
            ItemsList itemsList = insert.getItemsList();
            if (itemsList instanceof MultiExpressionList) {
                ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(getTenantHandler().getTenantId()));
            } else {
                ((ExpressionList) insert.getItemsList()).getExpressions().add(getTenantHandler().getTenantId());
            }
        } else {
            throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
        }
    }

    /**
     * 判断是否存在租户id列字段
     * @param insert
     * @return 如果已经存在,则绕过不执行
     */
    private boolean isAleadyExistTenantColumn(Insert insert) {
        List<Column> columns = insert.getColumns();
        if(CollectionUtils.isEmpty(columns)){
            return false;
        }
        String tenantIdColumn = getTenantHandler().getTenantIdColumn();
        return columns.stream().map(Column::getColumnName).anyMatch(tenantId -> tenantId.equals(tenantIdColumn));
    }

总结

以上三种方案如何选择?如果是项目初期阶段,推荐使用方案一,就是不要在业务层面直接去设置租户id,由租户插件统一处理。如果是全新项目,mybatis-plus推荐使用最新版。如果项目已经业务层面已经多处地方设置了租户id且mybatis-plus版本是3.4之前版本,推荐方案三直接扩展mybatis-plus的租户插件功能,就不推荐方案一了,避免漏改

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-mybatisplus-tenant

以上是关于如何解决使用mybatis-plus提供的多租户插件出现Column ‘tenant_id‘ specified twice问题的主要内容,如果未能解决你的问题,请参考以下文章

如何解决 Kubernetes 的多租户难题

mybatis-plus多租户的使用

带有 sequelize 和 nest.js 的多租户

休眠中的多租户数据库

详解ABP框架的多租户

只需几分钟,即可部署安全的多租户Kubernetes !