当MyBatis-Plus遇见多租户:用字段编织数据的平行宇宙

232 阅读3分钟

场景设定:假设我们正在开发一个SaaS化办公系统,每个企业租户的数据就像平行宇宙中的星球——彼此独立却共享同一片数据库星系。如何让SQL飞船在查询时自动导航到对应星球?MyBatis-Plus的租户拦截器就是我们的"曲速引擎"!


一、💡 多租户设计的"字段级隔离"模式

在字段级方案中,所有业务表都携带tenant_id字段(如同数据护照),MP通过SQL拦截器自动为每个CRUD操作加盖"签证章"。

优势

  • 像变色龙般融入现有表结构
  • 无需物理隔离的运维复杂度
  • 拦截器如同隐形管家,业务代码零侵入

二、🔧 核心装备:TenantLineInnerInterceptor

这个拦截器就像SQL的智能修正带,在语句生成时悄悄补上租户条件:

@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加租户拦截器(像给MP戴上AR眼镜)
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantHandler() {
            
            // 当前租户ID获取(可从ThreadLocal或SecurityContext获取)
            @Override
            public Expression getTenantId() {
                return new StringValue("T001"); // 示例写死,实际动态获取
            }

            // 租户字段名(每个表的签证印章位置)
            @Override
            public String getTenantIdColumn() {
                return "tenant_id";
            }

            // 是否需要进行租户过滤的表(白名单机制)
            @Override
            public boolean ignoreTable(String tableName) {
                return !"sys_user".equalsIgnoreCase(tableName); // 示例仅过滤用户表
            }
        }));
        return interceptor;
    }
}

三、🚀 让实体类携带"租户基因"

为实体类植入tenant_id字段,就像给每个数据对象颁发身份证:

@Data
@TableName("sys_user")
public class User {
    private Long id;
    private String name;
    
    @TableField(value = "tenant_id") 
    private String tenantId; // 租户标识基因片段
}

四、🌰 CRUD操作演示:隐形的数据国界

插入操作

User user = new User();
user.setName("张三");
userService.save(user); // 无需设置tenant_id!

生成SQL:

INSERT INTO sys_user (name, tenant_id) VALUES ('张三', 'T001')

查询操作

userService.lambdaQuery()
           .like(User::getName, "张")
           .list(); 

生成SQL:

SELECT * FROM sys_user 
WHERE name LIKE '%张%' 
AND tenant_id = 'T001'  -- 自动追加的租户边界

五、🔍 原理解密:拦截器的"魔法时刻"

  1. SQL解析:拦截器通过AST(抽象语法树)分析SQL结构
  2. 条件注入:在WHERE条件中插入tenant_id = ?
  3. 参数绑定:将当前租户值绑定到预处理语句
  4. 忽略策略:通过ignoreTable方法跳过系统表等特殊场景

整个过程如同在SQL生成流水线上安装了一个智能质检员,确保每个出厂的SQL都带有正确的租户标签。


六、⚠️ 注意事项:多租户开发的"生存法则"

  1. 租户ID获取:建议通过ThreadLocal或SecurityContext动态获取
  2. 数据初始化:手动插入数据时需主动填充tenant_id
  3. 复杂SQL:自定义SQL需使用${ew.customSqlSegment}保留租户条件
  4. 超级管理员:可通过@InterceptorIgnore(tenantLine = "true")跳过过滤
  5. 唯一性约束:需将tenant_id加入联合唯一索引

七、🚁 进阶技巧:在复杂查询中驾驭租户条件

当需要手动控制租户条件时,可使用Wrapper的租户方法:

// 临时切换租户上下文
try(TenantContext.switchTo("T002")){
    userService.list(new LambdaQueryWrapper<User>()
        .ne(User::getName, "管理员"));
}

// 忽略租户过滤(危险操作!)
userService.list(Wrappers.<User>lambdaQuery()
    .ignoreTenantCondition()); 

总结:通过MyBatis-Plus的租户拦截器,我们实现了业务代码与租户逻辑的星舰分离。这种设计让系统就像拥有自动导航功能,开发者只需专注业务逻辑,数据的平行宇宙自然有序运转。这种优雅的解耦,正是现代SaaS系统架构的艺术所在!