前言

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

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐