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