文章目录
- 前言
- 3 扩展功能
- 3.3 通用枚举
- 3.3.1 使用枚举类
- 3.3.2 功能测试
- 3.4 JSON类型处理器
- 3.4.1 使用JSON类型处理器
- 3.4.2 功能测试
- 3.5 配置加密
- 3.5.1 生成密钥
- 3.5.2 修改配置
- 3.5.3 功能测试
- 4 插件功能
- 4.1 自动分页插件
- 4.1.1 配置分页插件
- 4.1.2 分页API
- 4.2 通用分页实体
- 4.2.1 创建实体
- 4.2.2 开发接口
- 4.2.3 封装PageQuery工具方法
- 4.2.4 封装PageDTO工具方法
前言
MyBatisPlus详解系列文章:
MyBatisPlus详解(一)项目搭建、@TableName、@TableId、@TableField注解与常见配置
MyBatisPlus详解(二)条件构造器Wrapper、自定义SQL、Service接口
MyBatisPlus详解(三)lambdaQuery、lambdaUpdate、批量新增、代码生成、Db静态工具、逻辑删除
3 扩展功能
3.3 通用枚举
在User实体类中有一个状态字段status
:
对于这样的码表字段,一般会定义一个枚举类,做业务判断的时候直接基于枚举类进行比较。但是该字段在数据库采用的是int
类型,对应的实体类中是Integer
类型,因此业务操作时必须手动把枚举类与Integer
进行转换,相对麻烦。
为此,MybatisPlus提供了一个处理枚举类的类型转换器,可以对枚举类型与数据库类型进行自动转换。
3.3.1 使用枚举类
- 1)定义一个用户状态的枚举类UserStatus
// com.star.learning.enums.UserStatus
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
- 2)将User类中的
status
字段改为UserStatus类型
// com.star.learning.pojo.User
/**
* 使用状态(1正常 2冻结)
*/
//private Integer status;
private UserStatus status;
- 3)使用
@EnumValue
注解标记枚举属性:
要让MybatisPlus处理枚举类与数据库类型自动转换,就必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。MybatisPlus提供了@EnumValue
注解来标记枚举属性:
// com.star.learning.enums.UserStatus
@EnumValue
private final int value;
// ...
- 4)配置枚举处理器
在application.yaml文件中添加配置:
# src\main\resources\application.yaml
mybatis-plus:
configuration:
# 默认枚举类处理器(从3.5.2开始无需配置)
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
3.3.2 功能测试
执行前面几节编写好的/user/{id}
接口,根据id查询用户信息,可以发现查询出的User类的status
字段是枚举类型:
此时返回前端的信息是:
可见,status
字段JSON序列化后的值为NORMAL
,这显然是不符合要求的。
为此,MybatisPlus支持通过@JsonValue
注解,用于标记JSON序列化时展示的字段:
// com.star.learning.enums.UserStatus
@EnumValue
private final int value;
@JsonValue
private final String desc;
再次执行以上接口,返回前端的信息是:
3.4 JSON类型处理器
在数据库的t_user
表中,有一个类型为JSON
的字段info
(从MySQL5.7开始支持JSON
类型),保存的数据是JSON字符串:
在其对应的实体类User中,该字段是String
类型的:
这样设计时,查询到的info
字段就是一个JSON字符串,而要读取其中的属性,还需要将JSON字符串转换为对象。而写入数据库时,需要将对象转换为JSON字符串,较为繁琐。
为此,MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题,例如处理JSON问题就可以使用JacksonTypeHandler处理器。
3.4.1 使用JSON类型处理器
- 1)定义一个实体类UserInfo,对应
info
字段保存的信息:
// com.star.learning.pojo.UserInfo
@Data
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
- 2)将User类的
info
字段修改为UserInfo类型,并使用@TableField
注解声明该字段的类型处理器为JacksonTypeHandler,同时在实体类的@TableName
注解上添加autoResultMap = true
属性:
// com.star.learning.pojo.User
@Data
@TableName(value = "t_user", autoResultMap = true)
public class User {
// ...
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}
3.4.2 功能测试
调用/user/{id}
接口,根据id查询用户信息,可以发现查询出的User类的info
字段是UserInfo类型,且正确封装:
3.5 配置加密
目前在application.yaml配置文件中,很多参数都是明文的,例如数据库的用户名和密码。如果开发人员发生流动,则很容易导致敏感信息的泄露。为此,MybatisPlus支持配置文件的加密和解密功能。
3.5.1 生成密钥
利用AES工具生成一个随机秘钥,然后使用该密钥对数据库用户名、密码加密:
@Test
public void testAES() {
// 生成 16 位随机 AES 密钥
String randomKey = AES.generateRandomKey();
System.out.println("randomKey = " + randomKey);
// 利用密钥对用户名加密
String username = AES.encrypt("root", randomKey);
System.out.println("username = " + username);
// 利用密钥对用户名加密
String password = AES.encrypt("123456", randomKey);
System.out.println("password = " + password);
}
执行以上单元测试,控制台打印信息如下:
randomKey = cb3d37297a857ac9
username = nvC21NLjkZ8SToc+FxM7Jw==
password = p898OXS2GwKDdJI9P+GlGA==
3.5.2 修改配置
修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mybatis_plus_db?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
# 密文要以 mpw:开头
username: mpw:nvC21NLjkZ8SToc+FxM7Jw==
password: mpw:p898OXS2GwKDdJI9P+GlGA==
3.5.3 功能测试
启动项目后,调用/user/{id}
接口,发现报错:
java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up.
报错是因为在启动项目的时,需要把刚才生成的秘钥添加到启动参数中(以IDEA配置为例):
重新启动项目,再次调用/user/{id}
接口,发现可以正常访问数据库了。
4 插件功能
MybatisPlus提供了很多插件,进一步拓展其功能。目前已有的插件包括:
PaginationInnerInterceptor
:自动分页TenantLineInnerInterceptor
:多租户DynamicTableNameInnerInterceptor
:动态表名OptimisticLockerInnerInterceptor
:乐观锁IllegalSQLInnerInterceptor
:SQL性能规范BlockAttackInnerInterceptor
:防止全表更新与删除
使用最多的是自动分页插件。
4.1 自动分页插件
4.1.1 配置分页插件
在未配置分页插件的情况下,MybatisPlus是不支持分页功能的,IService接口和BaseMapper类中的分页方法都无法正常使用。
要配置分页插件,需要新建一个配置类:
// com.star.learning.config.MyBatisConfig
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
4.1.2 分页API
分页API的基本使用如下:
@Test
public void testPageQuery() {
// 1.分页查询,new Page()的两个参数分别是:页码、每页大小
Page<User> page = new Page<>(2, 2);
page = userService.page(page);
// 2.总条数
System.out.println("total = " + page.getTotal());
// 3.总页数
System.out.println("pages = " + page.getPages());
// 4.数据
List<User> records = page.getRecords();
records.forEach(System.out::println);
}
执行以上单元测试,控制台打印信息如下:
// 查询总条数
==> Preparing: SELECT COUNT(*) AS total FROM t_user
==> Parameters:
<== Total: 1
// 使用LIMIT进行分页查询
==> Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM t_user LIMIT ?,?
==> Parameters: 2(Long), 2(Long)
<== Total: 2
// 查询结果
total = 4
pages = 2
User(id=3, username=Hope, password=123, phone=13900112222, info=UserInfo(age=25, intro=上进青年, gender=male), status=NORMAL, balance=100000, createTime=2024-04-23T16:08:49, updateTime=2024-04-23T16:08:49, addressList=null)
User(id=4, username=Thomas, password=123, phone=17701265258, info=UserInfo(age=29, intro=伏地魔, gender=male), status=NORMAL, balance=800, createTime=2024-04-23T16:08:49, updateTime=2024-04-23T16:08:49, addressList=null)
分页的核心是Page类,除了可以设置当前页和每页大小,还支持排序参数,例如将以上单元测试修改如下:
// 1.分页查询,new Page()的两个参数分别是:页码、每页大小
Page<User> page = new Page<>(2, 2);
// 支持排序参数,第二个参数的true表示升序,false表示降序
page.addOrder(new OrderItem("balance", true));
// 支持多个排序参数
page.addOrder(new OrderItem("id", true));
// ...
再次执行以上单元测试,控制台打印信息如下:
==> Preparing: SELECT id, username, password, phone, info, status, balance, create_time, update_time FROM t_user ORDER BY balance ASC, id ASC LIMIT ?,?
==> Parameters: 2(Long), 2(Long)
<== Total: 2
可见,ORDER BY balance ASC, id ASC
已经被添加到了SQL语句中。
4.2 通用分页实体
下面要实现分页查询用户信息的接口:
参数 | 说明 |
---|---|
请求方式 | GET |
请求路径 | /user/page |
请求参数 | {"pageNo": 1, "pageSize": 5, "sortBy": "balance", "isAsc": false, "name": "o", "status": 1} |
返回值 | {"total": 100, "pages": 10, "list": [{"id": 1, "username": "user_1", "info": ...}]} |
特殊说明 | 如果排序字段为空,则默认按照更新时间排序 |
以上接口文档主要涉及3个实体类:
- UserQuery:分页查询条件的实体,包含分页、排序参数、查询条件
- PageDTO:分页结果实体,包含总条数、总页数、当前页数据
- User:用户信息实体
4.2.1 创建实体
- 1)创建一个PageQuery实体类,定义分页、排序参数:
// com.star.learning.pojo.PageQuery
@Data
public class PageQuery {
/** 页码 */
private Long pageNo;
/** 每页大小 */
private Long pageSize;
/** 排序字段 */
private String sortBy;
/** 是否升序 */
private Boolean isAsc;
}
- 2)修改UserQuery实体类,继承PageQuery类,这样UserQuery就即包含查询条件,又包含分页信息:
// com.star.learning.pojo.UserQuery
@Data
public class UserQuery extends PageQuery {...}
- 3)创建分页结果实体PageDTO类:
// com.star.learning.pojo.PageDTO
@Data
@AllArgsConstructor
public class PageDTO<T> {
/** 总条数 */
private Long total;
/** 总页数 */
private Long pages;
/** 数据集合 */
private List<T> list;
}
4.2.2 开发接口
- 1)在UserController中定义分页查询用户的接口:
// com.star.learning.controller.UserController
@GetMapping("/page")
public PageDTO<User> queryUsersPage(UserQuery query){
return userService.queryUsersPage(query);
}
- 2)在IUserService接口中创建
queryUsersPage
方法,在UserServiceImpl中实现该方法:
// com.star.learning.service.IUserService
PageDTO<User> queryUsersPage(UserQuery userQuery);
// com.star.learning.service.impl.UserServiceImpl
@Override
public PageDTO<User> queryUsersPage(UserQuery userQuery) {
// 1.构建条件
// 1.1.分页条件
Page<User> page = new Page<>(userQuery.getPageNo(), userQuery.getPageSize());
// 1.2.排序条件
if(userQuery.getSortBy() != null) {
page.addOrder(new OrderItem(userQuery.getSortBy(), userQuery.getIsAsc()));
} else {
// 默认按照更新时间降序排列
page.addOrder(new OrderItem("update_time", false));
}
// 2.查询数据
lambdaQuery()
.like(userQuery.getUsername() != null, User::getUsername, userQuery.getUsername())
.eq(userQuery.getStatus() != null, User::getStatus, userQuery.getStatus())
.ge(userQuery.getMinBalance() != null, User::getBalance, userQuery.getMinBalance())
.le(userQuery.getMaxBalance() != null, User::getBalance, userQuery.getMaxBalance())
.page(page);
// 3.数据处理
List<User> userList = page.getRecords();
if (userList == null || userList.isEmpty()) {
// 无数据,返回空结果
return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
}
// 有数据,返回
return new PageDTO<>(page.getTotal(), page.getPages(), userList);
}
- 3)启动项目,调用接口进行测试
控制台打印信息如下:
==> Preparing: SELECT COUNT(*) AS total FROM t_user WHERE (username LIKE ?)
==> Parameters: %o%(String)
<== Total: 1
==> Preparing: SELECT id, username, password, phone, info, status, balance, create_time, update_time FROM t_user WHERE (username LIKE ?) ORDER BY balance DESC LIMIT ?
==> Parameters: %o%(String), 2(Long)
<== Total: 2
可见,SQL语句实现了分页、排序和条件查询。
4.2.3 封装PageQuery工具方法
在上述代码中,从PageQuery类到MyBatisPlus的Page类之间的转换逻辑是这样的:
// com.star.learning.service.impl.UserServiceImpl#queryUsersPage()
// 1.构建条件
// 1.1.分页条件
Page<User> page = new Page<>(userQuery.getPageNo(), userQuery.getPageSize());
// 1.2.排序条件
if(userQuery.getSortBy() != null) {
page.addOrder(new OrderItem(userQuery.getSortBy(), userQuery.getIsAsc()));
} else {
// 默认按照更新时间降序排列
page.addOrder(new OrderItem("update_time", false));
}
这段代码还是比较麻烦的,并且每一个需要分页查询的Service方法都需要重写这一段转换逻辑。
为此,我们应该在PageQuery类中定义一个工具方法,完成PageQuery类到Page类之间的转换:
// com.star.learning.pojo.PageQuery
@Data
public class PageQuery {
/** 页码 */
private Long pageNo;
/** 每页大小 */
private Long pageSize;
/** 排序字段 */
private String sortBy;
/** 是否升序 */
private Boolean isAsc;
public <T> Page<T> toMpPage(OrderItem... orders){
// 1.分页条件
Page<T> p = Page.of(pageNo, pageSize);
// 2.排序条件
// 2.1.先看前端有没有传排序字段
if (sortBy != null) {
p.addOrder(new OrderItem(sortBy, isAsc));
return p;
}
// 2.2.再看有没有手动指定排序字段
if(orders != null){
p.addOrder(orders);
}
return p;
}
/**
* 指定排序字段和升降序
* @param defaultSortBy 排序字段
* @param isAsc 是否升序
*/
public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}
/**
* 默认create_time降序
*/
public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
return toMpPage("create_time", false);
}
/**
* 默认update_time降序
*/
public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
return toMpPage("update_time", false);
}
}
接下来改造UserServiceImpl类的queryUsersPage()
方法:
// com.star.learning.service.impl.UserServiceImpl#queryUsersPage()
// 1.构建条件:使用工具方法获得Page对象
Page<User> page = userQuery.toMpPageDefaultSortByUpdateTimeDesc();
// 2.查询数据......
4.2.4 封装PageDTO工具方法
在上述代码中,查询出结果后,从Page类到PageDTO类之间的转换逻辑是这样的:
// com.star.learning.service.impl.UserServiceImpl#queryUsersPage()
// ...
// 3.数据处理
List<User> userList = page.getRecords();
if (userList == null || userList.isEmpty()) {
// 无数据,返回空结果
return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
}
// 有数据,返回
return new PageDTO<>(page.getTotal(), page.getPages(), userList);
对于不同的Service方法,这一段代码逻辑应该是差不多的,因此可以把它封装成工具方法,在Service方法中直接调用:
// com.star.learning.pojo.PageDTO
@Data
@AllArgsConstructor
public class PageDTO<T> {
/** 总条数 */
private Long total;
/** 总页数 */
private Long pages;
/** 数据集合 */
private List<T> list;
/**
* 返回空分页结果
*/
public static <P> PageDTO<P> empty(Page<P> p){
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将MybatisPlus分页结果转为PO分页结果
* @return VO的分页对象
*/
public static <P> PageDTO<P> of(Page<P> p) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.isEmpty()) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据返回
return new PageDTO<>(p.getTotal(), p.getPages(), records);
}
}
接下来改造UserServiceImpl类的queryUsersPage()
方法,最终得到该方法的最简化版本:
// com.star.learning.service.impl.UserServiceImpl
@Override
public PageDTO<User> queryUsersPage(UserQuery userQuery) {
// 1.构建条件:使用工具方法获得Page对象
Page<User> page = userQuery.toMpPageDefaultSortByUpdateTimeDesc();
// 2.查询数据
lambdaQuery()
.like(userQuery.getUsername() != null, User::getUsername, userQuery.getUsername())
.eq(userQuery.getStatus() != null, User::getStatus, userQuery.getStatus())
.ge(userQuery.getMinBalance() != null, User::getBalance, userQuery.getMinBalance())
.le(userQuery.getMaxBalance() != null, User::getBalance, userQuery.getMaxBalance())
.page(page);
// 3.数据处理,使用工具方法返回数据
return PageDTO.of(page);
}
…
本节完,更多内容请查阅分类专栏:MyBatisPlus详解
本文涉及代码下载地址:https://gitee.com/weidag/mybatis_plus_learning.git
感兴趣的读者还可以查阅我的另外几个专栏:
- SpringBoot源码解读与原理分析(已完结)
- MyBatis3源码深度解析(已完结)
- Redis从入门到精通(已完结)
- 再探Java为面试赋能(持续更新中…)