在某些业务场景中,需要根据运行时条件动态选择数据库表名,例如:分表(按时间、租户、用户ID等)。MyBatis-Plus 提供了 DynamicTableNameInnerInterceptor
插件,配合 TableNameHandler
接口实现,可在 SQL 执行时动态替换表名。
一、核心概念
1. 什么是 TableNameHandler
?
TableNameHandler
是 MyBatis-Plus 动态表名功能的核心接口,用于在 SQL 执行时动态决定实际使用的表名。- 它由
DynamicTableNameInnerInterceptor
调用,根据当前上下文(如参数、时间、租户等)返回目标表名。 - 常用于:
- 日志表按月分表(如
log_202508
,log_202509
) - 用户行为表按用户 ID 哈希分表
- 多租户系统中按租户分库分表(配合多租户插件)
- 日志表按月分表(如
2. 核心方法
public interface TableNameHandler {
/**
* 根据当前 SQL 上下文动态返回实际表名
*
* @param sql 当前 SQL(可用于解析)
* @param tableName 原始表名(如 log_table)
* @return 实际要使用的表名(如 log_table_202508)
*/
String dynamicTableName(String sql, String tableName);
}
⚠️ 注意:该接口已从旧版的
Executor
模型迁移至基于InnerInterceptor
的新模型(3.4.0+)。
二、操作步骤(非常详细)
步骤 1:引入 MyBatis-Plus 依赖
确保使用 MyBatis-Plus 3.4.0 及以上版本(推荐 3.5.5):
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
步骤 2:定义实体类与 Mapper
假设有一个日志表,按月分表。
@TableName("log_record") // 这是逻辑表名,将被动态替换
@Data
public class LogRecord {
private Long id;
private String content;
private LocalDateTime createTime;
}
@Mapper
public interface LogRecordMapper extends BaseMapper<LogRecord> {}
🔔 注意:
@TableName("log_record")
是一个逻辑表名,实际表名为log_record_202508
等。
步骤 3:实现 TableNameHandler
接口
创建处理器,根据时间动态返回表名。
package com.example.handler;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class LogTableNameHandler implements TableNameHandler {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMM");
/**
* 根据当前时间动态返回日志表名
*
* @param sql 当前执行的 SQL
* @param tableName 原始表名(如 log_record)
* @return 实际表名(如 log_record_202508)
*/
@Override
public String dynamicTableName(String sql, String tableName) {
// 判断是否为日志相关表
if ("log_record".equalsIgnoreCase(tableName)) {
// 获取当前时间对应的年月
String suffix = LocalDateTime.now().format(FORMATTER);
return tableName + "_" + suffix; // 如 log_record_202508
}
// 其他表不处理,返回原表名
return tableName;
}
}
步骤 4:配置动态表名拦截器
创建 MyBatis-Plus 配置类,注册 DynamicTableNameInnerInterceptor
。
package com.example.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.example.handler.LogTableNameHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(LogTableNameHandler logTableNameHandler) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 创建动态表名拦截器
DynamicTableNameInnerInterceptor dynamicInterceptor = new DynamicTableNameInnerInterceptor();
// 方式一:注册单个处理器(推荐)
Map<String, TableNameHandler> map = new HashMap<>();
map.put("log_record", logTableNameHandler); // 逻辑表名 -> 处理器
dynamicInterceptor.setTableNameHandlerMap(map);
// 方式二:注册全局处理器(可选)
// dynamicInterceptor.setTableNameHandler((sql, tableName) -> {
// if ("log_record".equals(tableName)) {
// return "log_record_" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
// }
// return tableName;
// });
interceptor.addInnerInterceptor(dynamicInterceptor);
return interceptor;
}
}
✅ 推荐使用
Map<String, TableNameHandler>
方式,便于扩展和管理。
步骤 5:在业务中使用(无需特殊处理)
@Service
public class LogService {
@Autowired
private LogRecordMapper logRecordMapper;
public void saveLog(String content) {
LogRecord log = new LogRecord();
log.setContent(content);
log.setCreateTime(LocalDateTime.now());
// 插入操作将自动路由到 log_record_202508 表
logRecordMapper.insert(log);
}
public List<LogRecord> getLogs() {
// 查询也自动使用当前月表
return logRecordMapper.selectList(null);
}
}
步骤 6:验证效果
启动应用并调用 saveLog()
方法,查看实际执行 SQL:
INSERT INTO log_record_202508 (id, content, create_time) VALUES (?, ?, ?)
✅ 成功实现按月动态分表。
三、常见错误与解决方案
错误现象 | 原因 | 解决方案 |
---|---|---|
表名未替换,仍使用 log_record |
TableNameHandler 未注册或表名不匹配 |
检查 setTableNameHandlerMap 中的 key 是否与 @TableName 一致 |
启动报错找不到 Bean | LogTableNameHandler 未加 @Component |
添加注解或手动 @Bean 注册 |
多线程下表名错乱 | 使用了静态变量或非线程安全逻辑 | 确保 dynamicTableName 方法无状态 |
查询历史数据失败 | 只能查当前月 | 需手动指定表名或扩展逻辑(见技巧) |
四、注意事项
@TableName
必须是逻辑表名
实际表名由TableNameHandler
动态生成。不支持跨表查询
如SELECT * FROM log_record WHERE id IN (SELECT log_id FROM ...)
,若子查询涉及不同表名,可能出错。DDL 操作需提前建表
MyBatis-Plus 不负责建表,需通过脚本或工具提前创建log_record_202508
等表。缓存失效
不同表名视为不同 SQL,不影响一级/二级缓存。TableNameHandler
应无副作用
方法内不要修改全局状态。
五、使用技巧
技巧 1:支持查询历史表
// 在 Service 中手动设置上下文
public List<LogRecord> getLogsByMonth(int year, int month) {
String table = "log_record_" + String.format("%d%02d", year, month);
// 使用原生 SQL 或自定义 SQL 显式指定表名
return logRecordMapper.selectByTableName(table);
}
或通过 ThreadLocal 传递目标表名:
public class TableContext {
private static final ThreadLocal<String> TABLE = new ThreadLocal<>();
public static void setTable(String table) { TABLE.set(table); }
public static String getTable() { return TABLE.get(); }
public static void clear() { TABLE.remove(); }
}
在 TableNameHandler
中优先读取上下文:
String targetTable = TableContext.getTable();
if (targetTable != null) {
return targetTable;
}
// 否则走默认逻辑
技巧 2:按用户 ID 哈希分表
@Override
public String dynamicTableName(String sql, String tableName) {
if ("user_behavior".equals(tableName)) {
Long userId = UserContext.getUserId();
int shard = (userId.hashCode() & Integer.MAX_VALUE) % 4; // 4 个分表
return tableName + "_shard" + shard;
}
return tableName;
}
技巧 3:结合租户分表
可与 TenantLineHandler
配合,实现“租户 + 时间”二级分表:
return "log_" + tenantId + "_" + suffix;
六、最佳实践
实践 | 说明 |
---|---|
✅ 明确分表策略 | 按时间、哈希、范围等,避免随意分表 |
✅ 提前建表或自动建表 | 使用定时任务每月初创建新表 |
✅ 监控与告警 | 监控分表写入、查询性能 |
✅ 文档化表结构 | 明确各分表结构一致 |
✅ 避免频繁跨分表查询 | 必要时使用 ES 或数仓 |
✅ 使用连接池 | 分表不增加连接数,但高并发需优化池配置 |
七、性能优化
- 性能影响小:仅在 SQL 解析阶段替换表名,无额外开销。
- 避免反射或复杂计算:
dynamicTableName
方法应轻量。 - 缓存常用表名:如按月分表,可缓存最近 12 个月的表名映射。
- 批量操作支持:MyBatis-Plus 批量插入也支持动态表名(同一 SQL 使用同一表)。
- 索引优化:每个分表应有合理索引,避免全表扫描。
八、总结
项目 | 内容 |
---|---|
核心接口 | TableNameHandler |
拦截器 | DynamicTableNameInnerInterceptor |
配置方式 | setTableNameHandlerMap |
适用场景 | 按时间分表、哈希分表、多租户分表 |
优势 | 透明化分表,业务无感知 |
限制 | 不支持跨表复杂查询 |
✅ 一句话总结:
通过实现 TableNameHandler
并配置 DynamicTableNameInnerInterceptor
,MyBatis-Plus 可实现动态表名路由,是轻量级分表的优雅解决方案。
📌 立即实践建议:
- 定义逻辑表名实体。
- 实现
TableNameHandler
。 - 配置拦截器映射。
- 测试插入/查询是否自动路由到正确分表。
⚠️ 重要:分表策略一旦确定,不易变更,务必提前设计好!