1:适用场景及技术背景
存在多个系统级租户,在同一个数据库中,使用用同一套表来为每个租户提供彼此隔离的数据信息。通常需要设置一个租户字段,每次查询的时候根据租户字段进行条件筛查,如"where tenant_id = 123"。但是如果每次查询都进行手动拼接租户筛选条件就比较呆板且容易出错。因此我们可以借助Mybatis的插件机制以及多租户插件来实现无感知的多租户数据隔离
2:配置流程
- 在需要进行数据隔离的表中添加租户标识字段"tenant_id"
- 根据当前登录信息获取当前用户所属租户ID(获取当前用户信息的相关代码在此不做赘述),并将当前所属租户ID设置到租户上下文的本地线程变量中。代码如下:
@UtilityClass
public class TenantContextHolder {
/**
* 支持父子线程数据传递
*/
private final ThreadLocal<Long> THREAD_LOCAL_TENANT = new TransmittableThreadLocal<>();
/**
* 设置tenantId
*
* @param tenantId 租户ID
*/
public void setTenantId(Long tenantId) {
THREAD_LOCAL_TENANT.set(tenantId);
}
/**
* 获取租户ID
*
* @return Long
*/
public Long getTenantId() {
return THREAD_LOCAL_TENANT.get();
}
/**
* 清除tenantId
*/
public void clear() {
THREAD_LOCAL_TENANT.remove();
}
}
- 进行mybatis多租户插件的配置
package org.example.mytest.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import lombok.AllArgsConstructor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.NullValue;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Mybatis-plus插件,多租户插件
* 使用多租户插件时,执行insert语句会出现一下类型转换异常
* 问题出在TenantLineInnerInterceptor中的一下方法的PlainSelect plainSelect (PlainSelect)selectBody;行
*/
@Configuration
@AllArgsConstructor
@AutoConfigureBefore(MybatisPlusConfig.class)
@SuppressWarnings("all")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor tenantInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
//获取租户Id
@Override
public Expression getTenantId() {
Long tenantId= TenantContextHolder.getTenantId();
if (tenantId != null) {
return new LongValue(tenantId);
}
return new NullValue();
}
//获取多租户标识的字段名(重写了该方法,便于进行实际业务的个性化字段定义,源码中其实也是默认为tenant_id)
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
//过滤不需要根据tenant_id隔离的表,这是 default 方法,默认返回 false 表示所有表都需要拼多dept条件
@Override
public boolean ignoreTable(String tableName) {
//此处可以读取配置文件或自定义其他规则,进行忽略隔离操作
return false;
}
}));
return interceptor;
}
}
3:数据隔离测试
在项目中定义一个接口,并查询存在租户标识字段的表数据,并且可以通过上文中重写的"ignoreTable(String tableName)"方法的返回值,来控制是否需要进行数据隔离操作。测试代码如下:
@RestController
@RequestMapping("/test")
public class TestController{
@Resource
private TestMapper testMapper;
@PostMapping("/test")
public void test() {
List<TestEntity> list = testMapper.list(new LambdaQueryWrapper<TestEntity>());
System.out.println(list.size());
}
}
4:小结
在本文中,我们介绍了SpringBoot项目下,如何利用Mybatis提供的多租户插件来解决我们在实际业务中碰到的需要进行租户间数据隔离的问题。简单来说就是通过将TenantLineInnerInterceptor(多租户拦截器)配置到Mybatis的的拦截器链中,同时也可以通过实现TenantLineHandler接口中的方法,对我们的实际业务需求做出个性化的配置。但这样同时也带来了一个问题:在实际业务中,不可避免的存在需要忽略租户隔离的场景,但是我们的配置又是全局性的,或者全局生效,或者全局失效。