前言
本文案例来源于业务开发部门进行多租户开发时发生的案例。用过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 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