Mybatis Plus 动态表名(图文讲解)
一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
截止目前, 星球 内专栏累计输出 72w+ 字,讲解图 3103+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2400+ 小伙伴加入学习 ,欢迎点击围观
前言
大家好,我是小哈。
本小节中,我们将学习如何在 Mybatis Plus 中实现动态表名。*啥是动态表名呢?*我们都知道,当数据库单表存储数据太多时(比如单表超过 1000 万条数据),会严重影响查询效率,这就需要进行水平切分操作,将数据分别存储到不同表中。
举个栗子,假设某个产品预估将会增长到 3000 万用户,开发人员在设计阶段对用户表进行了分表,创建了 3 张表,通过用户 ID 进行模 3 操作,将数据均匀的存放到这 3 张表中,那么,在代码层处理时,需要动态生成表名。
数据库表与数据
为了演示,先新建三张用户表 t_user_0
、 t_user_1
和 t_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 ,有兴趣的小伙伴可参考下文:
新建参数传递辅助类
在项目 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 如下:
可以看到,实际将数据存到了 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 中实现动态表名,此功能通常会被应用于数据库分表场景中,希望通过对本文的学习,能对小伙伴们有所帮助!