首頁>技術>

前言

本文案例來源於業務開發部門進行多租戶開發時發生的案例。用過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

13
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • GraalVM 社群版 21.0.0釋出,高效能跨語言虛擬機器