话不多说上干货
最近接到一个需求 需要实现一个多租户的需求,实现租户之间的数据隔离,根据mp官网的资料,尝试采用mp官网的多租户插件来实现。
官网地址如下
多租户插件 | MyBatis-Plus
首先 创建一个 MybatisPlusConfig用来做mybatisplus的拦截器,话不多说上代码
@Slf4j
@Configuration
@MapperScan(basePackages = {"com.**.**.mapper"})
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//多租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(TenantContextHolder.getTenantId());
}
// 这里通过表名过滤当前不需要区分租户查询数据的表
@Override
public boolean ignoreTable(String tableName) {
return Arrays.asList("tenant"
).contains(tableName);
}
}){
@Override
protected void processInsert(Insert insert, int index, String sql, Object obj) {
//多租户只对查询进行动态拼接
}
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
//多租户只对查询进行动态拼接
}
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
//多租户只对查询进行动态拼接
}
});
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
return interceptor;
}
考虑到租户之间的数据隔离只针对于数据,所以我的想法是只对查询进行对应的租户筛选,所以禁用了修改 新增 删除的租户条件筛选操作,其实很好理解 各个租户只能看到自己的数据 所以也就只能删除对应的数据。
除此之外 我们需要对相关的表增加租户字段,这一点由于我的项目所有的实体都是有继承父类的,所以只需要在父类中添加租户字段即可,然后在对相关表进行添加租户字段,做到这里其实整体的思路已经有了
接下来需要做的就是,如何让租户添加的数据带有当前的租户id,我的实现思路是mp的自动填充字段功能
自动填充字段 | MyBatis-Plus
上代码
@Configuration
@Slf4j
public class MybatisCommonColumnsHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "tenantId", Long.class, TenantContextHolder.getTenantId());
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
需要注意的是自动填充功能只适用于mybatisplus,因此想要使用此功能需要在mapper上extends BaseMapper<class>,否则此功能无法生效。
然后我想大家可能都比较好奇上述代码中TenantContextHolder.getTenantId()这串代码是如何获取到租户id的,其实很简单
创建一个静态变量用来存储租户id
public class TenantContextHolder {
// 静态ThreadLocal变量,用于存储当前线程的租户ID
static ThreadLocal<Long> tenantThreadLocal = new ThreadLocal<>();
/**
* 获取当前线程的租户ID。
*
* @return 当前线程的租户ID
*/
public static Long getCurrentTenantId() {
return tenantThreadLocal.get();
}
/**
* 设置当前线程的租户ID。
*
* @param tenantId 要设置的租户ID
*/
public static void setCurrentTenantId(Long tenantId) {
tenantThreadLocal.set(tenantId);
}
/**
* 清除当前线程的租户ID,使得线程不再关联任何租户。
*/
public static void clear() {
tenantThreadLocal.remove();
}
public static Long getTenantId() {
if(TenantContextHolder.getCurrentTenantId()!=null)
{
Long tenantId = TenantContextHolder.getCurrentTenantId();
if(tenantId!=null)
{
return tenantId;
}
}
return 0L;
}
}
再去创建一个租户过滤器,拦截每次的请求去获取租户id
@Component
// 过滤器执行顺序注解,值越小优先级越高
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(urlPatterns = "/**", filterName = "TraceFilter")
public class TenantFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 从请求头中获取租户ID
String tenantId = request.getHeader("tenantId");
System.out.println("--------------------tenantId = " + tenantId);
TenantContextHolder.setCurrentTenantId(Long.valueOf(tenantId));
}
// 将租户ID转换为Long类型并设置到租户上下文 Threadlocal 中
filterChain.doFilter(request, response);
} finally {
// 清除租户上下文,避免线程池中线程复用导致的数据混乱
TenantContextHolder.clear();
}
}
}
这里提供两种思路,从请求中获取租户id,我们可以和前端商定每次请求中携带租户id,或者把租户id加进token,然后通过解析token获取租户id。
这样就可以实现租户id的获取了。
注意 有时候会遇到这样的场景 比如 某条查询语句不需要用到多租户条件去查询 所以我们需要对这条查询语句做特殊处理,mp官方也给出了相应的解决办法,就是在mapper层对查询语句做@InterceptorIgnore(tenantLine = "true")忽略多租户操作。这样这条查询语句的执行结果就会是不会携带租户id的查询了。
当然还有一种场景 比如在定时器中 上面的注解无法生效,或者 我们采用的查询方法是mp自生集成的方法比如在impl层直接this.list();不需要编写mapper层的方法。这种情况mp官方也提出了相应的方案,可以手动或略租户条件查询操作
// 设置忽略租户插件
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
// 执行逻辑 …
// 关闭忽略策略
InterceptorIgnoreHelper.clearIgnoreStrategy();
将你需要执行的操作放入手动操作的代码中,那么接下来执行的sql查询不会凭借租户id的查询条件,需要注意的是 完成该操作后需要关闭该操作。
InterceptorIgnoreHelper.clearIgnoreStrategy();
到这里我的分享就结束了,独自开发的路上踩到了许多坑,希望我的分享能对阅读你有所帮助。