MyBatis-Plus 动态表名的正确打开方式

解决的痛点

​ 在我们日常开发中,经常会遇到某个表的数据量非常大,需要按照年/月进行分表的情况。比如订单表、SN表等等。如何利用MybatisPlus的动态表名插件、以及如何进行使用,都比较繁琐。这里提供的动态表名的使用方式,是以MybatisPlus的动态表名插件为基础构建的。核心特性包括:

  • 基于 MyBatis-Plus 官方动态表名插件

  • 白名单机制,防止任意表名注入

  • ThreadLocal 作用域自动清理,避免线程污染

  • 提供 try-with-resources函数式 API 两种使用方式

使用方式

使用方式力求简洁,并且要保证在使用动态表名后,能动态清除表名。不留内存碎片。这里提供两个标准方式使用,示例中采用多数据源进行演示。

  • 指定表名并自动清除

    java 复制代码
    // 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);
  • 函数式表名并自动清除

    java 复制代码
    // 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项目中把实现方式列举出来。

动态表名白名单

为了想拦截需要进行动态的表名,这里采用配置文件中进行配置的方式。如果配置了就行拦截,否则也没有什么影响。

java 复制代码
/**
 * 配置的动态表名白名单
 *
 * @author 老马
 */
@Data
@ConfigurationProperties(prefix = "ums.database.dynamic-table")
public class DynamicTableProperties {

    /**
     * 允许使用动态表名的表(逻辑表名)
     */
    private Set<String> tables = new HashSet<>();
}

这里对应使用时的配置:

yaml 复制代码
ums:
  database:
    dynamic-table:
      tables:
        - t_xx_order
        - t_xx_sn

说明:

  • 这里配置的是 逻辑表名
  • 采用 前缀匹配策略
  • 示例中:
    • t_xx_order_2024
    • t_xx_order_2025 都会被允许
  • 如果使用了DynamicTableNameHelper类,但提供的又不是动态表名白名单中的表名,那么会提示错误

动态表名

该类,最重要的作用就是判断MybatisPlus的动态表名插件传入的表名是不是在配置的白名单中。

java 复制代码
/**
 * 动态表名白名单
 *
 * @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配置类

核心的配置类,这里重点关注初始化动态表名白名单和动态表名插件的处理。

Java 复制代码
/**
 * 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());
    }
}

动态表名助手类

java 复制代码
/**
 * 动态表名辅助类
 *
 * @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 成对执行
  • 避免线程池复用导致的表名污染问题
  • 另外还支持嵌套调用
java 复制代码
// 嵌套调用示例
try (Scope s1 = use("t_xx_order_2025")) {
    // 查询 2025
    try (Scope s2 = use("t_xx_order_2024")) {
        // 查询 2024
    }
    // 自动恢复为 2025
}

实体类

Java 复制代码
@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 中保存的动态表名 不会在线程之间自动传递

错误示例

java 复制代码
DynamicTableNameHelper.withTable("t_xx_order_2025", () -> {
    asyncService.doAsyncQuery(); // @Async 方法
});

上述代码中,doAsyncQuery 方法运行在新的线程中,此时动态表名上下文已经丢失,最终仍然会访问逻辑表名对应的默认表。

正确使用方式

java 复制代码
@Async
public void doAsyncQuery() {
    DynamicTableNameHelper.withTable("t_xx_order_2025", () -> {
        orderMapper.selectById(1L);
    });
}
相关推荐
Java水解2 小时前
springboot: Spring Boot 启动流程详解
spring boot·后端
马卡巴卡2 小时前
为什么Spring不建议使用@Autowired?@Resource才是王道
后端
martin10172 小时前
Oracle 11g 数据库卡顿排查与实战优化:一次真实的慢 SQL 定位全过程
数据库·后端
superman超哥2 小时前
Rust Cargo Run 与 Cargo Test 命令:开发工作流的双引擎
开发语言·后端·rust·cargo run·cargo test·开发工作流·双引擎
MMM_FanLe2 小时前
微博/朋友圈/点赞/评论系统设计
后端
架构精进之路2 小时前
AI 编程:重构工作流的思维与实践
后端·ai编程·trae
爬山算法2 小时前
Hibernate(9)什么是Hibernate的Transaction?
java·后端·hibernate
Craaaayon2 小时前
深入浅出 Spring Event:原理剖析与实战指南
java·spring boot·后端·spring