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