解决的痛点
在我们日常开发中,经常会遇到某个表的数据量非常大,需要按照年/月进行分表的情况。比如订单表、SN表等等。如何利用MybatisPlus的动态表名插件、以及如何进行使用,都比较繁琐。这里提供的动态表名的使用方式,是以MybatisPlus的动态表名插件为基础构建的。核心特性包括:
-
基于 MyBatis-Plus 官方动态表名插件
-
白名单机制,防止任意表名注入
-
ThreadLocal 作用域自动清理,避免线程污染
-
提供 try-with-resources 与 函数式 API 两种使用方式
使用方式
使用方式力求简洁,并且要保证在使用动态表名后,能动态清除表名。不留内存碎片。这里提供两个标准方式使用,示例中采用多数据源进行演示。
-
指定表名并自动清除
// A库中的2025年订单表 try (DynamicTableNameHelper.Scope ignore = DynamicTableNameHelper.use("t_xx_order_2025")) { OrderEntity order2025 = orderMapper.selectById(1984555429137637378L); System.out.println(order2025); } // A库中的2024年订单表 try (DynamicTableNameHelper.Scope ignore = DynamicTableNameHelper.use("t_xx_order_2024")) { OrderEntity order2024 = orderMapper.selectById(2001114675221204994L); System.out.println(order2024); } // 默认库中的商品表 ProductInfoEntity productInfo = this.productInfoMapper.selectById(1L); System.out.println(productInfo); -
函数式表名并自动清除
// A库中的2025年订单表 OrderEntity order2025 = DynamicTableNameHelper.withTable("t_xx_order_2025", () -> orderMapper.selectById(1984555429137637378L)); System.out.println(order2025); // A库中的2024年订单表 OrderEntity order2024 = DynamicTableNameHelper.withTable("t_xx_order_2024", () -> orderMapper.selectById(2001114675221204994L)); System.out.println(order2024); // 默认库中的产品表 ProductInfoEntity productInfoEntity = this.productInfoMapper.selectById(1L); System.out.println(productInfoEntity);
示例中特意采用了多数据源进行演示,目的想说明这个动态表名和多数据源之间并不冲突。
上面两种使用方式,没有好坏之分。仅仅是使用习惯而已。就我而且可能更倾向于使用代码更简洁的第2中方式。
| 使用方式 | 适合场景 |
|---|---|
| try-with-resources | 多条 SQL、复杂逻辑、跨方法调用 |
| withTable | 单次查询 / 插入 / 更新 |
如何做到
这里就要结合MybatisPlus的动态表名插件,所以这里会一步一步,在Springboot项目中把实现方式列举出来。
动态表名白名单
为了想拦截需要进行动态的表名,这里采用配置文件中进行配置的方式。如果配置了就行拦截,否则也没有什么影响。
/**
* 配置的动态表名白名单
*
* @author 老马
*/
@Data
@ConfigurationProperties(prefix = "ums.database.dynamic-table")
public class DynamicTableProperties {
/**
* 允许使用动态表名的表(逻辑表名)
*/
private Set<String> tables = new HashSet<>();
}
这里对应使用时的配置:
ums:
database:
dynamic-table:
tables:
- t_xx_order
- t_xx_sn
说明:
- 这里配置的是 逻辑表名
- 采用 前缀匹配策略
- 示例中:
t_xx_order_2024t_xx_order_2025都会被允许- 如果使用了DynamicTableNameHelper类,但提供的又不是动态表名白名单中的表名,那么会提示错误
动态表名
该类,最重要的作用就是判断MybatisPlus的动态表名插件传入的表名是不是在配置的白名单中。
/**
* 动态表名白名单
*
* @author 老马
*/
public class DynamicTables {
private static Set<String> TABLES = Collections.emptySet();
private DynamicTables() {
}
static void init(Set<String> tables) {
// 创建不可更改的Set
TABLES = Collections.unmodifiableSet(tables);
}
/**
* 是否允许使用动态表名
*/
public static boolean isDynamic(String tableName) {
if (!StringUtils.hasText(tableName)) {
return false;
}
return TABLES.stream().anyMatch(tableName::startsWith);
}
}
说明:
- 使用不可变
Set,避免运行期被修改- 通过前缀匹配支持多张物理分表
- 所有动态表名必须命中白名单
mybatis-plus配置类
核心的配置类,这里重点关注初始化动态表名白名单和动态表名插件的处理。
/**
* mybatis-plus配置类
*
* @author 老马
*/
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(DynamicTableProperties.class)
public class MybatisPlusConfig {
private final DynamicTableProperties dynamicTableProperties;
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 动态表名插件
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor());
// 其他插件
return interceptor;
}
/**
* 动态表名插件
*/
private DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor() {
TableNameHandler tableNameHandler = (sql, tableName) -> {
// 不在白名单,直接返回原表名
if (!DynamicTables.isDynamic(tableName)) {
return tableName;
}
// 取当前线程绑定的动态表名
String dynamicTableName = DynamicTableNameHelper.get();
// 没有设置动态表名,兜底返回原表名
return StringUtils.hasText(dynamicTableName)
? dynamicTableName
: tableName;
};
return new DynamicTableNameInnerInterceptor(tableNameHandler);
}
/**
* 初始化动态表名白名单
*/
@PostConstruct
public void initDynamicTables() {
DynamicTables.init(dynamicTableProperties.getTables());
}
}
动态表名助手类
/**
* 动态表名辅助类
*
* @author 银商北分-老马
* @since 1.0.0
*/
public final class DynamicTableNameHelper {
/**
* 表名正则
*/
public static final String TABLE_NAME_REGEX = "[a-zA-Z0-9_]+";
private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
private DynamicTableNameHelper() {}
/**
* 使用动态表名
*
* @param tableName 表名
* @return 作用域
*/
public static Scope use(String tableName) {
validate(tableName);
if (!DynamicTables.isDynamic(tableName)) {
throw new RuntimeException("表 [" + tableName + "] 未配置为允许动态表名");
}
String old = HOLDER.get();
HOLDER.set(tableName);
return () -> {
if (old == null) {
HOLDER.remove();
} else {
HOLDER.set(old);
}
};
}
/**
* 在指定的动态表名作用域内执行操作
*
* @param table 表名
* @param supplier 执行逻辑
* @param <T> 返回值类型
* @return 返回值
*/
public static <T> T withTable(String table, Supplier<T> supplier) {
try (Scope ignored = use(table)) {
return supplier.get();
}
}
/**
* 在指定的动态表名作用域内执行操作(无返回值)
*
* @param table 表名
* @param runnable 执行逻辑
*/
public static void withTable(String table, Runnable runnable) {
try (Scope ignored = use(table)) {
runnable.run();
}
}
/**
* 获取当前作用域的表名
*
* @return 表名
*/
public static String get() {
return HOLDER.get();
}
private static void validate(String tableName) {
if (tableName == null || tableName.isBlank()) {
throw new RuntimeException("tableName 不能为空");
}
if (!tableName.matches(TABLE_NAME_REGEX)) {
throw new RuntimeException("非法表名:" + tableName);
}
}
@FunctionalInterface
public interface Scope extends AutoCloseable {
/**
* 关闭作用域
*/
@Override
void close();
}
}
说明:
- 动态表名通过
ThreadLocal保存- 通过作用域模式确保 set / remove 成对执行
- 避免线程池复用导致的表名污染问题
- 另外还支持嵌套调用
// 嵌套调用示例
try (Scope s1 = use("t_xx_order_2025")) {
// 查询 2025
try (Scope s2 = use("t_xx_order_2024")) {
// 查询 2024
}
// 自动恢复为 2025
}
实体类
@Data
@TableName("t_xx_order")
public class OrderEntity implements Serializable {
/**
* 主键
*/
@TableId
private Long id;
/**
* 下单日期,格式:yyyy-MM-dd
*/
private String orderCreateDate;
/**
* 订单号
*/
private String orderno;
// ...省略其他属性
}
注意:
这里特别强调一下,这个动态表,一定要用@TableName注解告诉MybatisPlus的动态表名组件,逻辑表名叫什么。也就是我们这里的@TableName("t_xx_order")。否则无法拼接完成表名。默认MybatisPlus通过类,不会有前面的"t_xx_"。之后映射为order_entity,这种表名,那么在执行时就会报表或者视图不存在的错误了。
避坑指南
本方案的动态表名能力是基于 ThreadLocal 实现的,因此在使用时需要特别注意线程边界问题。
不支持的场景
以下场景中,动态表名不会自动生效,甚至可能出现查错表的风险:
@Async标注的方法- 手动使用线程池(
ExecutorService.submit / execute) CompletableFuture(使用默认或自定义线程池)- 任何发生 线程切换 的异步执行场景
原因在于:
ThreadLocal 中保存的动态表名 不会在线程之间自动传递。
错误示例
DynamicTableNameHelper.withTable("t_xx_order_2025", () -> {
asyncService.doAsyncQuery(); // @Async 方法
});
上述代码中,doAsyncQuery 方法运行在新的线程中,此时动态表名上下文已经丢失,最终仍然会访问逻辑表名对应的默认表。
正确使用方式
@Async
public void doAsyncQuery() {
DynamicTableNameHelper.withTable("t_xx_order_2025", () -> {
orderMapper.selectById(1L);
});
}