如何通过MyBatis拦截器在执行SQL前自动注入多租户物理隔离ID实现数据隔离?
- 内容介绍
- 相关推荐
本文共计704个文字,预计阅读时间需要3分钟。
直接使用MyBatis原生+Interceptor手动解析SQL并注入租户条件,风险极高、维护成本巨大。MyBatis-Plus内置的 TenantLineInnerInterceptor已覆盖所有标准DML场景(SELECT、INSERT、UPDATE、DELETE),经过大量SaaS生产环境验证,无需自行构建轮子。
TenantLineInnerInterceptor 怎么正确配置 tenant_id 注入逻辑
核心是实现 TenantLineHandler 接口,重点不在“怎么写”,而在“怎么取”和“怎么防错”:
-
getTenantId()必须返回Expression类型,不能直接 return 字符串或数字;常见错误是返回new StringValue("xxx")却没处理 null 场景——一旦TenantContext.getCurrentTenantId()为 null,整个 SQL 会崩掉 -
getTenantIdColumn()值必须与数据库表中真实字段名完全一致(大小写敏感),比如 MySQL 表定义是tenant_id,就不能写成TENANT_ID或tenantId -
ignoreTable(String tableName)要显式排除公共表,例如"sys_dict"、"sys_config";漏掉会导致这些表被错误加上WHERE tenant_id = ?,查不到全局数据
为什么 INSERT 语句能自动补 tenant_id 字段
插件不是简单拼字符串,而是用 JSQLParser 解析 AST 后,在 INSERT 的 column list 和 values list 中双向注入:
INSERT INTO user (name, email) VALUES (?, ?) → INSERT INTO user (name, email, tenant_id) VALUES (?, ?, ?)
但前提是你的实体类(如 User)里必须声明 tenant_id 字段,并加 @TableField(fill = FieldFill.INSERT) 注解,否则 MP 不知道该往哪填值。漏掉这个注解,INSERT 成功但数据无租户归属,等于裸奔。
租户 ID 来源必须绑定请求生命周期,不能靠静态变量
最常踩的坑:在 getTenantId() 里直接调用 SecurityContextHolder.getContext().getAuthentication() 或硬编码,导致异步线程、定时任务、单元测试中取不到值,或跨请求污染。
- 务必用
ThreadLocal封装TenantContext,并在 Web 层用OncePerRequestFilter或 Spring MVCHandlerInterceptor提前解析并 set - 解析来源优先级建议:Header
X-Tenant-ID> JWT payload 中的tenant_id> 子域名(如tenant1.example.com)> fallback 到默认租户(需明确日志告警) - 每次请求结束必须调用
TenantContext.clear(),否则 Tomcat 线程复用时会把上一个租户 ID 带进下一个请求
真正难的不是配置拦截器,而是确保租户上下文在所有执行路径中不丢失——包括 Dubbo 远程调用、RabbitMQ 消费、Scheduled 定时任务。这些地方都需要手动透传或重置 TenantContext,框架不会自动帮你做。
本文共计704个文字,预计阅读时间需要3分钟。
直接使用MyBatis原生+Interceptor手动解析SQL并注入租户条件,风险极高、维护成本巨大。MyBatis-Plus内置的 TenantLineInnerInterceptor已覆盖所有标准DML场景(SELECT、INSERT、UPDATE、DELETE),经过大量SaaS生产环境验证,无需自行构建轮子。
TenantLineInnerInterceptor 怎么正确配置 tenant_id 注入逻辑
核心是实现 TenantLineHandler 接口,重点不在“怎么写”,而在“怎么取”和“怎么防错”:
-
getTenantId()必须返回Expression类型,不能直接 return 字符串或数字;常见错误是返回new StringValue("xxx")却没处理 null 场景——一旦TenantContext.getCurrentTenantId()为 null,整个 SQL 会崩掉 -
getTenantIdColumn()值必须与数据库表中真实字段名完全一致(大小写敏感),比如 MySQL 表定义是tenant_id,就不能写成TENANT_ID或tenantId -
ignoreTable(String tableName)要显式排除公共表,例如"sys_dict"、"sys_config";漏掉会导致这些表被错误加上WHERE tenant_id = ?,查不到全局数据
为什么 INSERT 语句能自动补 tenant_id 字段
插件不是简单拼字符串,而是用 JSQLParser 解析 AST 后,在 INSERT 的 column list 和 values list 中双向注入:
INSERT INTO user (name, email) VALUES (?, ?) → INSERT INTO user (name, email, tenant_id) VALUES (?, ?, ?)
但前提是你的实体类(如 User)里必须声明 tenant_id 字段,并加 @TableField(fill = FieldFill.INSERT) 注解,否则 MP 不知道该往哪填值。漏掉这个注解,INSERT 成功但数据无租户归属,等于裸奔。
租户 ID 来源必须绑定请求生命周期,不能靠静态变量
最常踩的坑:在 getTenantId() 里直接调用 SecurityContextHolder.getContext().getAuthentication() 或硬编码,导致异步线程、定时任务、单元测试中取不到值,或跨请求污染。
- 务必用
ThreadLocal封装TenantContext,并在 Web 层用OncePerRequestFilter或 Spring MVCHandlerInterceptor提前解析并 set - 解析来源优先级建议:Header
X-Tenant-ID> JWT payload 中的tenant_id> 子域名(如tenant1.example.com)> fallback 到默认租户(需明确日志告警) - 每次请求结束必须调用
TenantContext.clear(),否则 Tomcat 线程复用时会把上一个租户 ID 带进下一个请求
真正难的不是配置拦截器,而是确保租户上下文在所有执行路径中不丢失——包括 Dubbo 远程调用、RabbitMQ 消费、Scheduled 定时任务。这些地方都需要手动透传或重置 TenantContext,框架不会自动帮你做。

