如何添加“默认给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

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

相关推荐
破烂pan2 小时前
Python 长连接实现方式全景解析
python·websocket·sse
高洁012 小时前
一文了解图神经网络
人工智能·python·深度学习·机器学习·transformer
用户3521802454752 小时前
🚀 Milvus 实战部署全记录
数据库·docker·ai编程
咸鱼加辣2 小时前
按“最近是否用过”删(LRU)
python
l1t2 小时前
PostgreSQL pg_clickhouse插件的安装和使用
数据库·clickhouse·postgresql·插件
serve the people3 小时前
tensorflow 零基础吃透:创建 tf.sparse.SparseTensor 的核心方法
人工智能·python·tensorflow
测试老哥3 小时前
UI自动化测试—Jenkins配置优化
自动化测试·软件测试·python·测试工具·ui·jenkins·测试用例
曲幽3 小时前
Python包管理告别龟速下载:uv工具国内镜像与离线安装实战
python·conda·pip·uv·venv·uvx