如何解決使用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM