如何添加“默认给Sql查询语句加上租户条件”的功能

从零实现"默认给 SQL 查询语句加上租户条件 "的功能,本质上是利用 MyBatis Plus 的插件机制配合 ThreadLocal 上下文来实现的。

我们需要构建一条完整的 "数据 -> 规则 -> 执行" 的链路。以下是标准化的 5 步实现指南


第一步:准备"背包" (定义上下文容器)

你需要一个地方在当前线程中存储"当前租户是谁"。

代码核心: 利用 ThreadLocal

java 复制代码
public class TenantContextHolder {
    // 存放当前租户ID
    private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();

    public static void setTenantId(Long tenantId) {
        TENANT_ID.set(tenantId);
    }

    public static Long getTenantId() {
        return TENANT_ID.get();
    }

    public static void clear() {
        TENANT_ID.remove();
    }
}

第二步:制定"规则" (实现 Handler 接口)

你需要告诉 MyBatis Plus 具体的过滤逻辑:租户ID是多少?列名叫什么?哪些表不需要加?

代码核心: 实现 TenantLineHandler 接口。

java 复制代码
@Component
public class MyTenantLineHandler implements TenantLineHandler {

    // 1. 告诉 MP,当前租户ID是多少 (从背包里拿)
    @Override
    public Expression getTenantId() {
        Long tenantId = TenantContextHolder.getTenantId();
        // 如果没拿到ID(比如没登录),返回 NullValue 可能会导致报错或查不到数据
        // 通常这里会做判空或者返回默认值
        return new LongValue(tenantId);
    }

    // 2. 告诉 MP,数据库里租户列的名字叫什么
    @Override
    public String getTenantIdColumn() {
        return "tenant_id";
    }

    // 3. 告诉 MP,哪些表需要忽略 (白名单)
    @Override
    public boolean ignoreTable(String tableName) {
        // A. 全局白名单:系统表(字典、菜单)不需要隔离
        if ("sys_dict".equals(tableName) || "sys_menu".equals(tableName)) {
            return true;
        }
        // B. 动态白名单:配合 @TenantIgnore 注解使用
        if (TenantContextHolder.isIgnore()) {
            return true;
        }
        // 默认:必须加过滤条件
        return false;
    }
}

第三步:组装"引擎" (配置 MyBatis 拦截器)

有了规则(Handler),你需要把它交给执行者(Interceptor),并把执行者放入 MyBatis 的插件链中。

代码核心: 配置 MybatisPlusInterceptor Bean。

java 复制代码
@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(MyTenantLineHandler tenantLineHandler) {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 核心动作:创建租户拦截器,并注入上面的规则 Handler
        // ⚠️注意:建议放在分页插件之前
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler));

        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        return interceptor;
    }
}

第四步:数据"注入" (配置 Web 过滤器)

引擎装好了,但还得有人把燃料(租户ID)塞进第一步的"背包"里。通常是在请求刚进来时处理。

代码核心: 实现 FilterHandlerInterceptor

java 复制代码
@Component
public class TenantContextFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        try {
            // 1. 从请求头 Header 获取 tenant-id
            HttpServletRequest req = (HttpServletRequest) request;
            String tenantIdStr = req.getHeader("tenant-id");
            
            if (tenantIdStr != null) {
                // 2. 塞进 ThreadLocal 背包
                TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
            }
            
            // 3. 放行,执行后续业务逻辑 (Service -> Mapper -> SQL拦截器)
            chain.doFilter(request, response);
            
        } finally {
            // 4. 【重要】请求结束,清空背包,防止线程污染
            TenantContextHolder.clear();
        }
    }
}

🌟 总结:这一套下来发生了什么?

  1. 请求进来 :Filter 从 Header 拿到 tenant_id=1,存入 TenantContextHolder
  2. 业务查询 :你写了 userMapper.selectList(null)
  3. 拦截改写TenantLineInnerInterceptor 拦截 SQL,调用 MyTenantLineHandler
  4. 读取规则 :Handler 从 TenantContextHolder 拿到 1
  5. SQL 变身 :SQL 被自动拼接为 SELECT * FROM user WHERE tenant_id = 1
  6. 请求结束 :Filter 清空 TenantContextHolder

这就是实现"全自动多租户隔离"的完整标准流程。

相关推荐
嗯嗯=1 天前
python学习篇
开发语言·python·学习
WoY20201 天前
opencv-python在ubuntu系统中缺少依赖
python·opencv·ubuntu
大游小游之老游1 天前
Python中如何实现一个程序运行时,调用另一文件中的函数
python
mantch1 天前
个人 LLM 接口服务项目:一个简洁的 AI 入口
人工智能·python·llm
weixin_445054721 天前
力扣热题51
c++·python·算法·leetcode
档案宝档案管理1 天前
档案宝自动化档案管理,从采集、整理到归档、利用,一步到位
大数据·数据库·人工智能·档案·档案管理
朱朱没烦恼yeye1 天前
java基础学习
java·python·学习
C_心欲无痕1 天前
浏览器缓存: IndexDB
前端·数据库·缓存·oracle
lkbhua莱克瓦241 天前
进阶-索引3-性能分析
开发语言·数据库·笔记·mysql·索引·性能分析
databook1 天前
数据可视化五大黄金原则:让你的图表“会说话”
python·数据分析·数据可视化