当使用 MyBatis Plus 的插件功能时,可以通过配置不同的插件来满足项目的需求。这些插件可以增强 MyBatis Plus 的功能,提高数据库操作的效率和安全性。以下是对主要插件的介绍:
插件 | 功能 |
---|---|
PaginationInnerInterceptor |
自动分页 |
TenantLineInnerInterceptor |
多租户 |
DynamicTableNameInnerInterceptor |
动态表名 |
OptimisticLockerInnerInterceptor |
乐观锁 |
IllegalSQLInnerInterceptor |
SQL 性能规范 |
BlockAttackInnerInterceptor |
防止全表更新与删除 |
PaginationInnerInterceptor
(自动分页):PaginationInnerInterceptor
用于实现自动分页功能。通过配置这个插件,可以轻松地在查询操作中启用分页,而无需手动编写分页查询的 SQL。插件会自动计算起始行和结束行,以获取指定范围的数据。
TenantLineInnerInterceptor
(多租户):TenantLineInnerInterceptor
插件用于支持多租户架构。在多租户环境中,不同租户共享同一数据库,但数据需要分隔。这个插件可以自动在 SQL 查询中添加租户条件,以确保每个租户只能访问自己的数据。
DynamicTableNameInnerInterceptor
(动态表名):DynamicTableNameInnerInterceptor
允许在运行时动态更改 SQL 查询中的表名。这对于根据不同的条件选择不同的表格非常有用,例如,可以根据用户身份或其他因素选择不同的表格。
OptimisticLockerInnerInterceptor
(乐观锁):OptimisticLockerInnerInterceptor
用于支持乐观锁机制。乐观锁是一种并发控制方式,可以防止多个用户同时修改同一记录。这个插件会自动为实体添加乐观锁的版本字段,并在更新操作中检查版本号,以确保数据的一致性。
IllegalSQLInnerInterceptor
(SQL 性能规范):IllegalSQLInnerInterceptor
用于检查 SQL 查询语句的性能规范。它可以帮助开发者优化 SQL 查询,减少潜在的性能问题。通过检查 SQL 的性能规范,可以提高查询的效率。
BlockAttackInnerInterceptor
(防止全表更新与删除):BlockAttackInnerInterceptor
用于防止执行全表更新和删除操作。这有助于减少潜在的危险操作,以保护数据库的安全性。插件可以拦截包含特定条件的 SQL 查询,阻止执行这些操作。
通过合理配置这些插件,可以提高 MyBatis Plus 的功能和性能,以满足不同项目的需求,并且这些插件使数据库操作更加高效和安全。下面将演示如何利用 PaginationInnerInterceptor
自动分页插件来实现分页功能。
在没有引入分页插件的情况下,Mybatis Plus 是不支持分页功能的,IService
和 BaseMapper
中的分页方法都无法正常生效。所以,我们必须配置分页插件,简单来说,就是创建一个配置类,然后注册一个 Bean 对象:
@Configuration
public class MyBatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
// 初始化 MybatisPlusInterceptor 核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加自动分页插件 PaginationInnerInterceptor
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 返回
return interceptor;
}
}
对上面代码的说明:
创建一个 Java 配置类 MyBatisConfig
并使用 @Configuration
注解标记它,这将使它成为一个 Spring Boot 的配置类。
通过 @Bean 注解,创建了一个名为 mybatisPlusInterceptor
的 Bean
对象,这个 Bean
是 Mybatis Plus 核心插件 MybatisPlusInterceptor
的实例。
在 mybatisPlusInterceptor
的 Bean 中,初始化 MybatisPlusInterceptor
的核心插件,并将分页插件 PaginationInnerInterceptor
添加到其中。
通过 interceptor.addInnerInterceptor(new PaginationInnerInterceptor())
语句,将分页插件添加到核心拦截器中,从而启用了分页功能。
通过这样的配置,MyBatis Plus 将能够正常支持分页功能,IService
和 BaseMapper
中的分页方法将生效。当执行带有分页参数的查询时,分页插件将自动计算起始行和结束行,并修改 SQL 查询语句,以获取指定范围的数据。
下面编写一个分页查询的测试类,查询第一页的数据,每一页最多两条记录:
@Test
void testPageQuery() {
// 1. 准备分页查询条件
Page<User> page = new Page<>();
// 1.1.设置分页参数
page.setCurrent(0).setSize(2);
// 2. 分页查询,最终返回的结果保存在 page 对象中
page = userService.page(new Page<>(0, 2));
// 3. 总条数
long total = page.getTotal();
// 4. 总页数
long pages = page.getPages();
System.out.println("总条数:" + total + " 总页数:" + pages);
// 5. 当前页数据
List<User> users = page.getRecords();
users.forEach(System.out::println);
}
对上面测试代码的解释:
在 testPageQuery
方法中,首先准备了一个分页查询条件。这是通过创建一个 Page
对象来实现的,其中 User
是要查询数据库表的实体类。
在分页查询条件中,设置了分页参数,包括当前页和每页记录数。在示例中,当前页为 0(第一页),每页最多显示 2 条记录。
接下来,使用 userService.page(new Page<>(0, 2))
执行分页查询。page()
方法是 MyBatis Plus 提供的分页查询方法。查询结果将存储在 page
对象中。
通过 page.getTotal()
获取总记录数,这是符合查询条件的所有记录数量。
通过 page.getPages()
获取总页数,这是根据分页参数计算得出的。
最后,通过 page.getRecords()
获取当前页的数据列表,并通过 forEach
方法遍历打印每条记录。
这个测试代码用于验证分页功能是否正常工作,以及是否返回了正确的分页结果。
最终运行结果:
另外,这里用到的分页参数Page
,既可以支持分页参数,也可以支持排序参数。例如按照 balance
进行降序排序:
// 1. 准备分页查询条件
Page<User> page = new Page<>();
// 1.1.设置分页参数
page.setCurrent(0).setSize(2);
// 1.2.设置排序规则
page.addOrder(new OrderItem("balance", false));
其中OrderItem
类就是用于设置排序规则,第一个参数用于指定排序的字段,第二个参数则指定是否是升序排序,设置 false
则为降序。
现在要实现一个用户分页查询的接口,接口规范如下:
请求方式:GET
请求路径:/users/page
请求参数:
{
"pageNo": 1,
"pageSize": 5,
"sortBy": "balance",
"isAsc": false,
"name": "o",
"status": 1
}
响应数据格式:
{
"total": 100006,
"pages": 50003,
"list": [
{
"id": 1685100878975279298,
"username": "user_9****",
"info": {
"age": 24,
"intro": "英文老师",
"gender": "female"
},
"status": "正常",
"balance": 2000
}
]
}
特殊说明:
要实现上面这个接口,首先需要定义 3 个实体类:
UserQuery
:分页查询条件的实体,包含分页、排序参数、过滤条件PageDTO
:分页结果实体,包含总条数、总页数、当前页数据UserVO
:用户页面视图实体UserQuery
之前已经定义过了,并且其中已经包含了过滤条件,具体代码如下:
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
其中缺少的仅仅是分页条件,而分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此最好将分页查询条件单独定义为一个PageQuery
实体。
PageQuery
是前端提交的分页查询参数,一般包含以下四个属性:
pageNo
:页码pageSize
:页面大小sortBy
:排序字段isAsc
:是否升序下面是 PageQuery
实体类的实现代码:
@Data
@ApiModel(description = "分页查询实体类")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("页面大小")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
然后,让 UserQuery
继承这个实体:
这里使用了 Lombok
提供的注解 @EqualsAndHashCode
来重写 equals
和 hashCode
方法。
最后,我们需要实现PageDTO
实体,用于返回分页查询的结果,由于后面可能会针对多张表进行分页查询,因此这里将其实现为泛型类,具体的实现代码如下:
@Data
@ApiModel("分页结果实体")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("当前页数据集合")
private List<T> list;
}
当完成了上述所有实体类的准备之后,就可以定义分页查询的接口了。
首先使用 Controller 方法,在 UserController
中新增一个 queryUsersPage
接口:
@Api(tags = "用户管理接口")
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private IUserService userService;
// ...
@GetMapping("/page")
@ApiOperation("根据复杂条件分页查询用户接口")
public PageDTO<UserVO> queryUsersPage(UserQuery query){
return userService.queryUsersPage(query);
}
}
实现 Service 接口
IUserService
中定义 queryUsersPage
方法
public interface IUserService extends IService<User> {
// ...
PageDTO<UserVO> queryUsersPage(UserQuery query);
}
UserServiceImpl
中实现 queryUsersPage
方法@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
// 1. 准备分页查询条件
// 1.1.分页条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
// 1.2.排序条件
if(query.getSortBy().isEmpty()){
// 如果排序字段为空,按照更新时间排序
page.addOrder(new OrderItem("update_time", false));
} else {
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
}
// 2. 查询,查询的接口将封装到 page 对象中。
this.page(page);
// 3. 数据非空校验
List<User> users = page.getRecords();
if(users == null || users.isEmpty()){
return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
}
// 4. 存在数据,进行实体类转换
List<UserVO> list = BeanUtil.copyToList(users, UserVO.class);
// 5. 封装返回结果
return new PageDTO<>(page.getTotal(), page.getPages(), list);
}
虽然通过上面的操作之后,实现了通用分页实体,但是我们发现在实现的queryUsersPage
方法中有很大篇幅的代码,像其中的准备分页条件、最终结果的转换等等基本都是通用的代码,如果要实现其他表的分页查询功能的时候,这些代码就又需要再写一遍。因此我们可以考虑将这一部分代码封装起来。
让我们首先来改造准备分页条件的代码,在刚才的代码中,从 PageQuery
到 Mybatis Plus的 Page
之间转换的过程还是比较麻烦的。因此我们可以直接将这段代码封装到 PageQuery
实体中,作为一个工具方法来简化开发。
例如:
@ApiModel(description = "分页查询实体类")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("页面大小")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
/**
* 将 PageQuery 转化为 Mybatis Plus Page
*
* @param orderItems 手动设置排序条件
* @param 泛型
* @return Mybatis Plus Page
*/
public <T> Page<T> toMpPage(OrderItem... orderItems) {
// 1. 分页条件
Page<T> p = Page.of(pageNo, pageSize);
// 2. 排序提交
if (sortBy != null) {
p.addOrder(new OrderItem(sortBy, isAsc));
return p;
}
if (orderItems != null) {
p.addOrder(orderItems);
}
return p;
}
// 手动传入排序方式
public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc) {
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}
// 默认 按照 CreateTime 降序排序
public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
return this.toMpPage("create_time", false);
}
// 默认 按照 UpdateTime 降序排序
public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
return this.toMpPage("update_time", false);
}
}
这样我们在编写 Service 代码的时候就可以省去对从 PageQuery
到 Page
的转换了:
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
在查询出分页结果后,数据的非空校验,数据的 VO 转换同样都是模板代码,编写起来很麻烦。因此我们同样可以将其封装到 PageDTO
的工具方法中,简化整个过程:
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("分页结果实体")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("当前页数据集合")
private List<T> list;
/**
* 返回空的分页结果
* @param p MyBatis Plus 的分页结果
* @param 目标 VO 类型
* @param 原始的 PO 类型
* @return VO 的分页结果
*/
public static <V, P> PageDTO<V> empty(Page<P> p) {
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将 MyBatis Plus 的分页结果转换为 VO 的分页结果
* @param p MyBatis Plus 的分页结果
* @param voClass 目标 VO 类型的字节码
* @param 目标 VO 类型
* @param 原始的 PO 类型
* @return VO 的分页结果
*/
public static <V, P>PageDTO<V> of(Page<P> p, Class<V> voClass){
// 1. 非空校验
List<P> records = p.getRecords();
if(records == null || records.isEmpty()){
// 无数据,返回空结果
return empty(p);
}
// 2. 数据转换
List<V> vos = BeanUtil.copyToList(records, voClass);
// 3. 封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
/**
* 将 Mybatis Plus分页结果转为 VO分页结果,允许用户自定义 PO到 VO的转换方式
* @param p Mybatis Plus的分页结果
* @param convertor PO 到 VO的转换函数
* @param 目标 VO 类型
* @param 原始 PO 类型
* @return VO 的分页结果
*/
public static <V, P>PageDTO<V> of(Page<P> p, Function<P, V> convertor){
// 1. 非空校验
List<P> records = p.getRecords();
if(records == null || records.isEmpty()){
// 无数据,返回空结果
return empty(p);
}
// 2. 数据转换
List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
// 3. 封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
}
完成了上面所有的功能之后,最后 queryUsersPage
的实现方法就变成了:
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
// 1. 准备分页条件
Page<User> p = query.toMpPageDefaultSortByCreateTimeDesc();
// 2. 分页查询
this.page(p);
// 3. 封装返回
return PageDTO.of(p, UserVO.class);
}
此时,三行代码就完成了,并且如果要对其他数据库表进行分页查询,也通用能够复用这些代码。
另外,如果是希望自定义 PO 到 VO 的转换过程,可以这样做:
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
// 1. 准备分页条件
Page<User> p = query.toMpPageDefaultSortByCreateTimeDesc();
// 2. 分页查询
this.page(p);
// 3. 封装返回
return PageDTO.of(p, user -> {
// 拷贝属性到 VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
// 用户名脱敏处理
String username = userVO.getUsername();
userVO.setUsername(username.substring(0, username.length() - 2) + "**");
// 返回 userVO 对象
return userVO;
});
}
最终查询的返回结果:
发现成功对用户名实现了脱敏操作: