Mybatis Plus 动态表名(图文讲解)

更新时间 2023-02-03 17:13:39

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡 / 赠书活动

目前, 星球 内第一个项目:全栈前后端分离博客项目,演示地址:http://116.62.199.48/, 1.0 版本已经更新完毕,正在更新 2.0 版本。采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 224 小节,累计 35w+ 字,讲解图:1544 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 1000+ 小伙伴加入,欢迎点击围观

前言

大家好,我是小哈。

本小节中,我们将学习如何在 Mybatis Plus 中实现动态表名。*啥是动态表名呢?*我们都知道,当数据库单表存储数据太多时(比如单表超过 1000 万条数据),会严重影响查询效率,这就需要进行水平切分操作,将数据分别存储到不同表中。

举个栗子,假设某个产品预估将会增长到 3000 万用户,开发人员在设计阶段对用户表进行了分表,创建了 3 张表,通过用户 ID 进行模 3 操作,将数据均匀的存放到这 3 张表中,那么,在代码层处理时,需要动态生成表名。

数据库表与数据

为了演示,先新建三张用户表 t_user_0t_user_1t_user_2 ,并通过 user_id 字段进行取模存储:

建表语句如下:

CREATE TABLE `t_user_0` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID(主键)',
  `name` varchar(30) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `gender` tinyint(2) NOT NULL DEFAULT '0' COMMENT '性别,0:女 1:男',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

CREATE TABLE `t_user_1` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID(主键)',
  `name` varchar(30) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `gender` tinyint(2) NOT NULL DEFAULT '0' COMMENT '性别,0:女 1:男',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

CREATE TABLE `t_user_2` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID(主键)',
  `name` varchar(30) NOT NULL DEFAULT '' COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `gender` tinyint(2) NOT NULL DEFAULT '0' COMMENT '性别,0:女 1:男',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

注意,因为水平分表的缘故,为了保证注解的唯一性,所以三张表的实际主键为 user_id 字段,在插入数据时,通过对用户 ID 取模(userId % 3)将数据放到对应的表中。

接下来,分别向 3 张表中插入几条测试数据:

INSERT INTO `t_user_0` (`id`, `user_id`, `name`, `age`, `gender`)
VALUES
 (1, 1616332498122366978, '犬小哈', 30, 1);

INSERT INTO `t_user_1` (`id`, `user_id`, `name`, `age`, `gender`)
VALUES
 (1, 1616332498571157505, '犬小哈', 30, 1);

INSERT INTO `t_user_2` (`id`, `user_id`, `name`, `age`, `gender`)
VALUES
 (1, 1616332498319499266, '犬小哈', 20, 1);

开始

数据准备好了以后,新建一个 Spring Boot 示例工程。

示例工程结构

先放一张工程结构的截图,后续会一一创建每个类:

添加依赖

在 Maven 的 pom.xml 配置文件中,添加如下依赖:

        <!-- lombok,免写繁琐的 get/set 等方法 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- mybatis-plus 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <!-- mysql 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- p6spy 组件,可以完整打印实际执行 SQL -->
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>

yml 配置

application.yml 文件中添加数据库相关配置:

spring:
  datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://127.0.0.1:3306/master_db?characterEncoding=utf-8
    username: root
    password: 123456

TIP : 小哈这里使用到了 p6spy 组件,用于打印完整的执行 SQL ,有兴趣的小伙伴可参考下文:

《Mybatis Plus 打印 SQL 语句(包含执行耗时)》

新建参数传递辅助类

在项目 config 包下新建 RequestDataHelper 类,用于传递动态表名所需参数:

/**
 * 请求参数传递辅助类
 */
public class RequestDataHelper {
    /**
     * 请求参数存取
     */
    private static final ThreadLocal<Map<String, Object>> REQUEST_DATA = new ThreadLocal<>();

    /**
     * 设置请求参数
     *
     * @param requestData 请求参数 MAP 对象
     */
    public static void setRequestData(Map<String, Object> requestData) {
        REQUEST_DATA.set(requestData);
    }

    /**
     * 获取请求参数
     *
     * @param param 请求参数
     * @return 请求参数 MAP 对象
     */
    public static <T> T getRequestData(String param) {
        Map<String, Object> dataMap = getRequestData();
        if (CollectionUtils.isNotEmpty(dataMap)) {
            return (T) dataMap.get(param);
        }
        return null;
    }

    /**
     * 获取请求参数
     *
     * @return 请求参数 MAP 对象
     */
    public static Map<String, Object> getRequestData() {
        return REQUEST_DATA.get();
    }
}

新建 Mybatis Plus 配置类

在项目 config 包下,添加 Mybatis Plus 配置类:

@Configuration
@MapperScan("com.example.mybatisplusdemo.mapper")
public class MybatisPlusConfig {
    /**
     * 添加动态表名插件
     **/
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
            // 获取参数方法
            Map<String, Object> paramMap = RequestDataHelper.getRequestData();
            if (CollectionUtils.isNotEmpty(paramMap)) {
                paramMap.forEach((k, v) -> System.err.println(k + "----" + v));
				// 获取传递的参数
                Long userId = (Long) paramMap.get("user_id");
                // 水平分表 3 张,对 ID 进行取模决定表名后缀
                String tableNameSuffix = "_" + userId % 3;
                // 组装动态表名
                return tableName + tableNameSuffix;
            }
            return tableName;
        });
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        // 3.4.3.2 作废该方式
        // dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
        return interceptor;
    }

}

注意:考虑到项目中可能会添加多个插件,需要注意顺序关系,官方推荐顺序如下:

多租户插件 -> 动态表名插件 -> 分页插件 -> 乐观锁插件 -> sql 性能规范插件 -> 防止全表更新与删除插件。

新建数据库实体类

@Data
@Builder
@TableName("t_user")
public class User {
    private Long id;
    @TableId(type = IdType.ASSIGN_ID)
    private Long userId;
    private String name;
    private Integer age;
    private Integer gender;
}

新建 Mapper

在项目的 mapper 包下,新建 UserMapper 接口:

public interface UserMapper extends BaseMapper<User> {
}

单元测试

完成上面的前制工作后,我们新建单元测试,测试一下动态表名是否生效。

添加数据

    @Test
    void testInsert() {
        // 此用户 ID 取模(模3)等于 0,应存于 t_user_0 表中
        Long userId = Long.valueOf("1616332498055258115");
        // 传递动态表名所需参数
        RequestDataHelper.setRequestData(new HashMap<String, Object>() {{
            put("user_id", userId);
        }});

        User user = User.builder().name("犬小哈").age(30).gender(1).build();
        userMapper.insert(user);
    }

执行单元测试,实际执行 SQL 如下:

Mybatis Plus 插入数据(动态表名)Mybatis Plus 插入数据(动态表名)

可以看到,实际将数据存到了 t_user_0 表中,这说明动态表名插件生效了,并且按照代码定义的,对 user_id 进行了取模(模 3)。

查询数据

    @Test
    void testSelectUser() {
        // 传递动态表名所需参数, 对此用户 ID 进行模 3,结果为 1,应从 t_user_1 表中查询
        RequestDataHelper.setRequestData(new HashMap<String, Object>() {{
            put("user_id", 1616332498571157505L);
        }});

        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(User::getUserId, 1616332498571157505L);
        User user = userMapper.selectOne(wrapper);
        System.out.println(user);
    }

执行该单元测试,实际执行 SQL 如下,完美,一切正常工作:

Mybatis Plus 查询数据(动态表名)Mybatis Plus 查询数据(动态表名)

结语

本小节中,我们学习了如何在 Mybatis Plus 中实现动态表名,此功能通常会被应用于数据库分表场景中,希望通过对本文的学习,能对小伙伴们有所帮助!