适用于分表分库、按月分表、多租户独立表等场景,实现运行时动态替换表名。
一、核心概念
- 动态表名
运行时根据规则动态生成表名(如order_2023
→order_2024
)。 - 表名处理器(TableNameHandler)
自定义表名生成规则的核心接口。 - 动态表名拦截器(DynamicTableNameInnerInterceptor)
在 SQL 执行前替换原始表名。
二、详细操作步骤
1. 添加依赖(确认版本 ≥ 3.4.0)
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version> <!-- 使用最新版本 -->
</dependency>
2. 实现表名处理器
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
public class DynamicTableNameHandler implements TableNameHandler {
// ThreadLocal 保存动态表名参数
private static final ThreadLocal<String> TABLE_SUFFIX = new ThreadLocal<>();
public static void setSuffix(String suffix) {
TABLE_SUFFIX.set(suffix);
}
public static void clear() {
TABLE_SUFFIX.remove();
}
@Override
public String dynamicTableName(String sql, String originalTable) {
// 动态拼接表后缀 (e.g. order -> order_2024)
String suffix = TABLE_SUFFIX.get();
return suffix != null ? originalTable + "_" + suffix : originalTable;
}
}
3. 配置动态表名拦截器
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 创建动态表名拦截器
DynamicTableNameInnerInterceptor dynamicInterceptor = new DynamicTableNameInnerInterceptor();
// 2. 配置表名处理规则 (key=逻辑表名, value=处理器)
Map<String, TableNameHandler> handlerMap = new HashMap<>();
handlerMap.put("order", new DynamicTableNameHandler()); // 所有order表动态替换
handlerMap.put("user", new UserTableNameHandler()); // 自定义其他处理器
dynamicInterceptor.setTableNameHandlerMap(handlerMap);
// 3. 添加到拦截器链
interceptor.addInnerInterceptor(dynamicInterceptor);
return interceptor;
}
}
4. 业务层动态设置表名参数
@Service
public class OrderService {
public List<Order> get2024Orders() {
try {
// 关键:设置当前线程表名后缀
DynamicTableNameHandler.setSuffix("2024");
// 执行查询 (自动使用 order_2024 表)
return orderMapper.selectList(new QueryWrapper<>());
} finally {
// 必须清理ThreadLocal,防止内存泄漏
DynamicTableNameHandler.clear();
}
}
}
5. 处理分表字段(可选)
在实体类中添加分表字段(非数据库字段):
public class Order {
private Long id;
private String orderNo;
@TableField(exist = false) // 非数据库字段
private String tableSuffix; // 分表标识字段
}
三、常见错误与解决
表名未动态替换
- 检查拦截器配置顺序(动态表名拦截器需在分页插件前添加)
- 确认
handlerMap
中的逻辑表名与 SQL 中的表名大小写一致
ThreadLocal 污染
- 必须在
finally
块中执行clear()
- 异步场景使用
TransmittableThreadLocal
- 必须在
复杂 SQL 解析失败
- 避免在 SQL 中使用保留字作为表别名
- 升级 MyBatis-Plus 到最新版
NPE 异常
- 在
dynamicTableName()
方法中处理空指针:public String dynamicTableName(String sql, String originalTable) { return Optional.ofNullable(TABLE_SUFFIX.get()) .map(suffix -> originalTable + "_" + suffix) .orElse(originalTable); }
- 在
四、注意事项
拦截器顺序
动态表名拦截器必须在分页插件(PaginationInnerInterceptor
)之前添加:interceptor.addInnerInterceptor(dynamicInterceptor); // 动态表名第一 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页第二
多表关联处理
多表 JOIN 时需手动指定动态表名:SELECT * FROM order_${year} o JOIN user_${year} u ON o.user_id = u.id
DDL 语句跳过
自动建表语句需跳过动态表名替换:public String dynamicTableName(String sql, String originalTable) { if (sql.toUpperCase().startsWith("CREATE TABLE")) { return originalTable; // 跳过DDL } // ...动态替换逻辑 }
五、使用技巧
多维度分表策略
public String dynamicTableName(String sql, String table) { // 根据用户ID分表:user_uid123 if ("user".equals(table)) { return table + "_" + UserContext.getUserId(); } // 根据月份分表:order_202405 if ("order".equals(table)) { return table + "_" + YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM")); } return table; }
注解驱动动态表名
自定义注解实现更精细控制:@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DynamicTable { String value(); // 表后缀 } // AOP切面 @Around("@annotation(dynamicTable)") public Object setDynamicTable(ProceedingJoinPoint pjp, DynamicTable dynamicTable) { DynamicTableNameHandler.setSuffix(dynamicTable.value()); try { return pjp.proceed(); } finally { DynamicTableNameHandler.clear(); } }
分表路由配置化
通过配置中心动态更新路由规则:public class ConfigurableTableHandler implements TableNameHandler { private final TableRouter router; // 从配置中心读取规则 @Override public String dynamicTableName(String sql, String table) { return router.route(table); // 例: order -> order_${shardKey} } }
六、最佳实践与性能优化
分表键设计原则
- 选择高基数字段(如 user_id)
- 避免热点数据倾斜(如按时间分表需加随机后缀)
SQL 预编译优化
- 动态表名应在拦截器阶段完成替换
- 避免在 XML 中使用
${tableName}
破坏预编译
跨表查询处理
- 并行查询+内存聚合:
List<CompletableFuture<List<Order>>> futures = tables.stream() .map(table -> CompletableFuture.supplyAsync(() -> queryByTable(table))) .toList(); List<Order> result = futures.stream() .flatMap(future -> future.join().stream()) .collect(Collectors.toList());
- 使用 UNION ALL 合并查询(需相同表结构)
- 并行查询+内存聚合:
元数据缓存
缓存表结构信息避免每次解析:public class CachedTableHandler implements TableNameHandler { private final Map<String, String> tableCache = new ConcurrentHashMap<>(); @Override public String dynamicTableName(String sql, String table) { return tableCache.computeIfAbsent(table, this::calculateRealTable); } private String calculateRealTable(String logicTable) { // 复杂计算逻辑... } }
影子表压测支持
动态切换到压测表:public String dynamicTableName(String sql, String table) { if (isStressTesting()) { return table + "_stress"; // order -> order_stress } // ...正常逻辑 }
七、完整流程图
graph TD A[业务调用] --> B{设置ThreadLocal<br>表名参数} B --> C[执行Mapper方法] C --> D[MyBatis-Plus拦截器链] D --> E[DynamicTableNameInnerInterceptor] E --> F{查询表名处理器<br>Map} F -->|存在处理器| G[调用dynamicTableName()] G --> H[拼接真实表名] H --> I[替换原始SQL表名] I --> J[执行修改后SQL] J --> K[返回结果] F -->|无处理器| L[使用原始表名] L --> J K --> M[清除ThreadLocal]
通过以上配置,可轻松实现:
- 按时间分表(日志/订单)
- 按租户分表(SaaS系统)
- 按业务分片(大数据表)
- 动态压测影子表
关键点:
- 正确使用
ThreadLocal
传递参数并确保清理 - 复杂 SQL 做好兼容性测试
- 分表策略避免热点问题