注意:代码有更新,在MyTenantLineInnerInterceptor类的构造方法中加入一行 super.setTenantLineHandler(tenantLineHandler); 否则更新会报错!!!
一、多租户实现
基于MyBatis plus官方提供的多租户插件可以很容易的实现多租户数据隔离功能,只需要在需要数据隔离的表结构中加上相应的租户字段即可,简单实现如下文:
使用@Configuration定义一个MybatisPlusConfig配置类,随后定义MybatisPlusInterceptor bean由Spring管理,在bean中实现分页插件以及多租户插件,需要注意需要先定义多租户拦截器,随后定义分页插件,这样不会出现只有select多租户隔离生效,分页不生效的问题
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue("租户ID");
}
@Override
public String getTenantIdColumn() {
return "industry_chain_id";
}
@Override
public boolean ignoreTable(String tableName) {
List<String> list = new ArrayList<>();
list.add("ods_whs_zsj_wh_corp_enterp_indi_bsc_info");
list.add("t_talent");
return list.stream().noneMatch(s -> s.equals(tableName));
}
}));
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
上述代码为简单实现多租户数据隔离功能,在getTenantId方法中获取当前请求的租户ID,如果是使用Spring Security的可以定义的Context中,如果是其他的可以在filter中编写代码存储到ThreadLocal中,在请求结束后清除,做法很多都可以实现获取当前请求的租户ID;getTenantIdColumn是获取租户列名的,需要在数据库需要隔离的表中加上租户字段,我这里是industry_chain_id;ignoreTable用于判定是否忽略多租户查询条件,比如一些系统表不需要多租户过滤,只给超管用,那么在这边可以进行表名的判断,返回为true即表示忽略多租户查询条件。
二、多租户的特殊需求
当我在用上述方法实现了多租户数据隔离后,某一天客户突然找到我,说需要支持某条数据同时存在于多个租户中,也就是说A用户既可能在A企业中也可能在B企业中,所以要求当租户ID为A的时候或者为B的时候,A用户都可以被查出来,而表中实现的方式是基于逗号分割,比如A用户的industry_chain_id存储的数据为A,B,这样该如何实现呢?
尝试
首先看到TenantLineHandler类中getTenantId方法是获取租户ID的方法,随后在打印的SQL中看到SQL为select * from t_user where industry_chain_id = ?
,当时理解为我写的new LongValue拼接字段后面是=相连,于是想,结合我表中存的租户字段,使用like不就可以了吗,既然getTenantId是定义多租户值的,那是不是可以在这边下功夫呢?于是我点进Expression类,尝试找出符合需求的实现类,于是找到了LikeExpression
点进LikeExpression类后发现能够set值的字段不多,
看了半天也就看懂一个withLeftExpression与withRightExpression可能是我们需要用到的,于是我改掉getTenantId方法为
@Override public Expression getTenantId() { // 使用 LikeExpression 构建租户条件 LikeExpression likeExpression = new LikeExpression(); likeExpression.withRightExpression(new LongValue(SecurityUser.getIndustryChainId())); return likeExpression; }
尝试在右操作数设置租户值实现like查询,随后打印SQL发现执行语句为select * from t_user where industry_chain_id = '' like ?
为什么=始终存在呢,随后我又尝试了很多方法比如设置左操作数、给右操作数拼接%%,但是都没有作用
无法实现,分析源码
首先我们看到,MyBatis plus的多租户功能是add一个TenantLineInnerInterceptor到MybatisPlusInterceptor,我们点进TenantLineInnerInterceptor中发现类里面自己维护了一个TenantLineHandler,而我们实现的时候也是给TenantLineInnerInterceptor类一个自己重写后的TenantLineHandler类,但是很明显只操作TenantLineHandler类无法实现我们的需求,于是阅读代码,发现一个方法
很明显是在这个类中实现的一些SQL执行前的拼接,而从源码中我们看到buildTableExpression
方法,这个方法只有一行,而且调用了我们重写的tenantLineHandler类中的方法,那我想这个一定是拼接SQL的关键,阅读代码我们发现,判断当前SQL的表名是否存在于ignoreTable的忽略清单中,如果忽略,则不构建表达式,返回NULL,否则返回new EqualsTo类,从名称中我们可以看出这个类一定跟相等有关系,观察它的两个参数,第一个参数为获取字段别名并拼接的一些方法,里面也获取到了我们的getTenantIdColumn字段,目前来看只是设置SQL的字段名的,跟我们的需求没有关系;看第二个参数,第二个参数是获取当前请求的租户值,所以大概可以推测出这个表达式构建的是"字段名=值"的SQL,点进EqualsTo类后发现果然是实现的Expression类,那么如何解决就知道了,在buildTableExpression
方法中下功夫
最终实现
首先我们可以看到TenantLineInnerInterceptor的buildTableExpression
方法是允许被重写的,于是我们定义一个类,继承TenantLineInnerInterceptor,我们只需要重写buildTableExpression
方法,其他的仍然由MyBatis plus进行操作,而我们仍然是需要获取字段名的,所以copy一份getAliasColumn
方法到我们自定义的拦截器类中,而buildTableExpression
方法全部重写,
@Override
public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
if (this.tenantLineHandler.ignoreTable(table.getName())) {
return null;
}
Expression expression = this.tenantLineHandler.getTenantId();
LikeExpression likeExpression = null;
if (expression instanceof LikeExpression) {
likeExpression = (LikeExpression) expression;
likeExpression.withLeftExpression(this.getAliasColumn(table));
}
return likeExpression;
}
首先我们需要仍然按照原先方法中的来,第一步忽略不需要添加多租户过滤条件的表,返回NULL,第二步获取tenantLineHandler传过来的Expression类,也就是getTenantId方法,因为我们只需要实现基于like的模糊查询,所以我判断Expression是否是LikeExpression类?如果是的话,强制转换为LikeExpression,随后设置左操作数为我们的租户列名,而右操作数在原本传过来的Expression方法中存在,所以不需要再设置,随后返回Expression类给MyBatis plus,其他操作不重写,保持原样。随后也重写了tenantLineHandler类,代码如下:
public class MyTenantLineHandler implements TenantLineHandler {
@Override
public Expression getTenantId() {
LikeExpression likeExpression = new LikeExpression();
likeExpression.withRightExpression(new StringValue("%" + SecurityUser.getIndustryChainId() + "%"));
return likeExpression;
}
@Override
public String getTenantIdColumn() {
return "industry_chain_id";
}
/**
* 这个地方需要排除没有这个权限字段的表,如果是嵌套或者连表,都会添加上这个多租户
*
* @param tableName
* @return
*/
@Override
public boolean ignoreTable(String tableName) {
List<String> list = new ArrayList<>();
list.add("ods_whs_zsj_wh_corp_enterp_indi_bsc_info");
list.add("t_talent");
return list.stream().noneMatch(s -> s.equals(tableName));
}
public String getUserName() {
return SecurityUser.getManager().getUsername();
}
}
仍然是跟原先MybatisPlusInterceptor类中重写的方法一致,只不过在getTenantId方法中我们构建了右操作数为我们的租户值,在前后加上了%%实现任意模糊查询,在我们自定义的MyTenantLineInnerInterceptor类中会给Expression构建左操作数即列名,MybatisPlusInterceptor类中改为下方代码即可。
interceptor.addInnerInterceptor(new MyTenantLineInnerInterceptor(new MyTenantLineHandler()));
最后奉上代码:
public class MyTenantLineInnerInterceptor extends TenantLineInnerInterceptor {
private MyTenantLineHandler tenantLineHandler;
public TenantLineHandler getTenantLineHandler() {
return this.tenantLineHandler;
}
public void setTenantLineHandler(final MyTenantLineHandler tenantLineHandler) {
this.tenantLineHandler = tenantLineHandler;
}
public MyTenantLineInnerInterceptor() {
}
public MyTenantLineInnerInterceptor(final MyTenantLineHandler tenantLineHandler) {
super.setTenantLineHandler(tenantLineHandler);
this.tenantLineHandler = tenantLineHandler;
}
@Override
public Expression buildTableExpression(Table table, Expression where, String whereSegment) {
if (this.tenantLineHandler.ignoreTable(table.getName())) {
return null;
}
Expression expression = this.tenantLineHandler.getTenantId();
LikeExpression likeExpression = null;
if (expression instanceof LikeExpression) {
likeExpression = (LikeExpression) expression;
likeExpression.withLeftExpression(this.getAliasColumn(table));
}
return likeExpression;
}
protected Column getAliasColumn(Table table) {
StringBuilder column = new StringBuilder();
if (table.getAlias() != null) {
column.append(table.getAlias().getName()).append(".");
}
column.append(this.tenantLineHandler.getTenantIdColumn());
return new Column(column.toString());
}
}
public class MyTenantLineHandler implements TenantLineHandler {
@Override
public Expression getTenantId() {
LikeExpression likeExpression = new LikeExpression();
likeExpression.withRightExpression(new StringValue("%" + SecurityUser.getIndustryChainId() + "%"));
return likeExpression;
}
@Override
public String getTenantIdColumn() {
return "industry_chain_id";
}
/**
* 这个地方需要排除没有这个权限字段的表,如果是嵌套或者连表,都会添加上这个多租户
*
* @param tableName
* @return
*/
@Override
public boolean ignoreTable(String tableName) {
List<String> list = new ArrayList<>();
list.add("ods_whs_zsj_wh_corp_enterp_indi_bsc_info");
list.add("t_talent");
return list.stream().noneMatch(s -> s.equals(tableName));
}
public String getUserName() {
return SecurityUser.getManager().getUsername();
}
}
三、最终结论
从上述过程中可以了解到MyBatis plus多租户插件的工作原理,也实现了重写buildTableExpression来构建我们需要的多租户查询语句,如果想要实现当前租户为超管则不添加多租户过滤条件也是可以的,同样在buildTableExpression方法中,判断当前用户是否为超管即可,为true则返回NULL,如果某些方法需要忽略多租户但是表不需要,则在mapper方法上加@InterceptorIgnore(tenantLine = "true")
注解即可。