MyBatis-Plus 动态表名的正确使用方式

 更新时间:2026年02月28日 10:33:00   作者:老马9527  
本文主要介绍了MyBatis-Plus 动态表名的正确使用方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

解决的痛点

​ 在我们日常开发中,经常会遇到某个表的数据量非常大,需要按照年/月进行分表的情况。比如订单表、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_2024
    • t_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);
    });
}

到此这篇关于MyBatis-Plus 动态表名的正确使用方式的文章就介绍到这了,更多相关MyBatisPlus 动态表名内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Log4j 配置日志打印时区的实现方法

    Log4j 配置日志打印时区的实现方法

    下面小编就为大家分享一篇Log4j 配置日志打印时区的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • Spring中Bean的三种实例化方式详解

    Spring中Bean的三种实例化方式详解

    这篇文章主要给大家介绍了关于Spring中实例化bean的三种方式:构造方法、静态工厂和实例工厂,对我们学习有一定的参考价值,需要的小伙伴可以了解一下
    2022-06-06
  • Springmvc调用存储过程,并返回存储过程返还的数据方式

    Springmvc调用存储过程,并返回存储过程返还的数据方式

    这篇文章主要介绍了Springmvc调用存储过程,并返回存储过程返还的数据方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • 带你了解Java数据结构和算法之队列

    带你了解Java数据结构和算法之队列

    这篇文章主要为大家介绍了Java数据结构和算法之队列,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • Java自旋锁与读写锁的实现原理

    Java自旋锁与读写锁的实现原理

    本文介绍了Java中的自旋锁和读写锁,自旋锁是一种非阻塞锁,适用于锁持有时间极短的场景,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2026-02-02
  • 如何解决Idea断点调试乱跳的问题

    如何解决Idea断点调试乱跳的问题

    这篇文章主要介绍了如何解决Idea断点调试乱跳的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • SpringBoot中@FeignClient 注解的作用

    SpringBoot中@FeignClient 注解的作用

    Feign可以帮助我们定义和实现服务之间的 RESTful 接口,使得服务之间的调用更加方便和可靠,本文主要介绍了SpringBoot中@FeignClient 注解的作用,感兴趣的可以了解一下
    2024-06-06
  • MybatisPlus使用注解的多对多级联查询方式

    MybatisPlus使用注解的多对多级联查询方式

    这篇文章主要介绍了MybatisPlus使用注解的多对多级联查询方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • java调用外部程序的方法及代码演示

    java调用外部程序的方法及代码演示

    这篇文章主要介绍了java调用外部程序的方法及代码演示的相关资料,需要的朋友可以参考下
    2023-03-03
  • 深入了解Java语言中的并发性选项有何不同

    深入了解Java语言中的并发性选项有何不同

    这篇文章主要介绍了深入了解Java语言中的并发性选项有何不同,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,,需要的朋友可以参考下
    2019-06-06

最新评论