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 动态表名内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解JNA中的回调方法

    详解JNA中的回调方法

    这篇文章主要介绍了JNA中的回调方法,主要包括JNA 中的 Callback,callback 的应用,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2022-05-05
  • 浅谈一下Spring中的createBean

    浅谈一下Spring中的createBean

    createBean是创建Bean的主要方法, 该方法位于:AbstractBeanFactory的doGetBean方法中的createBean调用。本文就来浅谈一下Spring中的createBean ,感兴趣的可以了解一下
    2022-07-07
  • 创建网关项目(Spring Cloud Gateway)过程详解

    创建网关项目(Spring Cloud Gateway)过程详解

    这篇文章主要介绍了创建网关项目(Spring Cloud Gateway)过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • Spring事务失效的8大原因详解

    Spring事务失效的8大原因详解

    这篇文章主要介绍了Spring事务失效的8大原因详解,这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB,需要的朋友可以参考下
    2023-09-09
  • java读取图片并转化为二进制字符串的实现方法

    java读取图片并转化为二进制字符串的实现方法

    这篇文章主要介绍了java读取图片并转化为二进制字符串的实例代码,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-09-09
  • Spring Cloud应用实现配置自动刷新过程详解

    Spring Cloud应用实现配置自动刷新过程详解

    这篇文章主要介绍了Spring Cloud应用实现配置自动刷新过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • java通过客户端访问服务器webservice的方法

    java通过客户端访问服务器webservice的方法

    这篇文章主要介绍了java通过客户端访问服务器webservice的方法,涉及java创建与调用webservice的相关技巧,需要的朋友可以参考下
    2016-08-08
  • Mybatis的mapper.xml中if标签test判断的用法说明

    Mybatis的mapper.xml中if标签test判断的用法说明

    这篇文章主要介绍了Mybatis的mapper.xml中if标签test判断的用法说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • 详解Spring依赖注入:@Autowired,@Resource和@Inject区别与实现原理

    详解Spring依赖注入:@Autowired,@Resource和@Inject区别与实现原理

    这篇文章主要介绍了详解Spring依赖注入:@Autowired,@Resource和@Inject区别与实现原理,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-06-06
  • Java 常量与变量的区别详细介绍

    Java 常量与变量的区别详细介绍

    这篇文章主要介绍了Java 常量与变量的区别的相关资料,并附实例代码帮助大家学习理解,需要的朋友可以参考下
    2016-10-10

最新评论