系列博客目录
众多插件
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
- PaginationInnerInterceptor:自动分页
- TenantLineInnerInterceptor:多租户
- DynamicTableNameInnerInterceptor:动态表名
- OptimisticLockerInnerInterceptor:乐观锁
- IllegalSQLInnerInterceptor:sql 性能规范
- BlockAttackInnerInterceptor:防止全表更新与删除
注意:
使用多个插件的时候需要注意插件定义顺序,建议使用顺序如下:
- 多租户,动态表名
- 分页,乐观锁
- sql 性能规范,防止全表更新与删除
这里我们以分页插件为里来学习插件的用法,最常用。MP没有限制分页的方式的,也可以使用之前的PageHelper来实现,不过需要额外引入PageHelper的依赖,既然我们已经用来MP的依赖,我们为了方便可以直接使用MP的分页。
分页插件
在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效。所以,我们必须配置分页插件。
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件。
在项目中新建一个配置类:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.annotation.DbType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1. 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 2. new分页插件 DbType.MYSQL 表示 使用的是 MySQL 数据库
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 设置分页查询的最大限制
pageInterceptor.setMaxLimit(1000L); // 设置分页上限为 1000
// 将分页插件添加到拦截器中,也可以添加其他MP插件
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
IService
中的分页代码
第一个函数的第二个参数就是Wrapper查询条件,第一个参数page有个泛型E,E就是前面定义好的E extends IPage,就是IPage这个接口以及其子类类型(如下图所示)。
用的最多的就是Page,Page里面包含分页的参数,页码,每页的大小,排序方式等信息。
所以调用分页函数的时候,首先传分页的参数,再传查询条件。
函数的返回值也是E,其实和PageHelper一样,传的参数不光是参数还有分页结果,只不过传的时候没有结果而已,填好之后再返回带有结果的E。
/**
* 翻页查询
*
* @param page 翻页对象
* @param queryWrapper 实体对象封装操作类 {@link com.baomidou.mybatisplus.core.conditions.query.QueryWrapper}
*/
default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper) {
return getBaseMapper().selectPage(page, queryWrapper);
}
/**
* 无条件翻页查询
*
* @param page 翻页对象
* @see Wrappers#emptyWrapper()
*/
default <E extends IPage<T>> E page(E page) {
return page(page, Wrappers.emptyWrapper());
}
现在可以使用了
@Test
void testPageQuery() {
// 1. 设置分页参数
int pageNo = 1; // 当前页码
int pageSize = 2; // 每页显示的记录数
// 1.1 创建分页对象,使用 Page.of 方法来指定当前页和每页记录数
Page<User> page = Page.of(pageNo, pageSize);
// 1.2 设置排序条件,通过 OrderItem 来指定排序字段和排序方式
page.addOrder(new OrderItem("balance", false)); // 根据 balance 字段降序排序
// 1.3 执行分页查询,获取分页数据
Page<User> p = userService.page(page);
// 2. 打印总记录数
System.out.println("Total records: " + p.getTotal()); // 获取查询的总记录数
// 3. 打印总页数
System.out.println("Total pages: " + p.getPages()); // 获取总页数
// 4. 获取当前页的数据
List<User> records = p.getRecords(); // 获取当前页的记录
// 5. 遍历并打印当前页的数据
records.forEach(System.out::println);
}
案例
简单分页查询案例。需求:遵循下面的接口规范,编写一个UserController接口,实现User的分页查询
请求方式
- GET
请求路径
/users/page
请求参数
{
"pageNo": 1, // 当前页码,表示从第几页开始查询
"pageSize": 5, // 每页显示的记录数
"sortBy": "balance", // 排序字段,表示根据哪个字段进行排序
"isAsc": false, // 排序方式,true 表示升序,false 表示降序
"name": "o", // 用户名筛选条件,支持模糊查询
"status": 1 // 用户状态筛选条件,1 表示正常状态
}
返回值
{
"total": 100006, // 总记录数
"pages": 50003, // 总页数
"list": [ // 当前页的用户列表
{
"id": 1685100878975279298, // 用户 ID
"username": "user_9****", // 用户名
"info": { // 用户详细信息
"age": 24, // 用户年龄
"intro": "英文老师", // 用户简介
"gender": "female" // 用户性别
},
"status": "正常", // 用户状态
"balance": 2000 // 用户余额
}
]
}
特殊说明
- 排序字段为空:如果
sortBy
字段为空,则默认按照更新时间进行排序。 - 排序字段不为空:如果
sortBy
字段有值,则根据该字段进行排序,排序顺序由isAsc
字段决定(true
为升序,false
为降序)。
分析一下:查询的参数一般会构建一个查询实体来存,返回的参数可以通过VO或者DTO来存放。
实体
这里需要定义3个实体:
- UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件
- PageDTO:分页结果实体,包含总条数、总页数、当前页数据
- UserVO:用户页面视图实体
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@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:是否升序
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Long pageNo;
@ApiModelProperty("页码")
private Long pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
然后,让我们的UserQuery继承这个实体:
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery extends PageQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
返回值的用户实体沿用之前定一个UserVO实体:
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("详细信息")
private String info;
@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;
@ApiModelProperty("账户余额")
private Integer balance;
@ApiModelProperty("收货地址列表")
private List<AddressVO> addresses;
}
最后,则是分页实体PageDTO:
package com.itheima.mp.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
}
开发接口
我们在UserController
中定义分页查询用户的接口:
package com.itheima.mp.controller;
import com.itheima.mp.domain.dto.PageDTO;
import com.itheima.mp.domain.query.PageQuery;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/page")
public PageDTO<UserVO> queryUsersPage(UserQuery query){
return userService.queryUsersPage(query);
}
// 。。。 略
}
然后在IUserService
中创建queryUsersPage
方法:
PageDTO<UserVO> queryUsersPage(PageQuery query);
接下来,在UserServiceImpl
中实现该方法:
@Override
public PageDTO<UserVO> queryUsersPage(PageQuery query) {
// 1.构建条件
// 1.1.分页条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
// 1.2.排序条件
if (query.getSortBy() != null) {
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
}else{
// 默认按照更新时间排序
page.addOrder(new OrderItem("update_time", false));
}
// 2.查询
page(page);
// 3.数据非空校验
List<User> records = page.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
}
// 4.有数据,转换
List<UserVO> list = BeanUtil.copyToList(records, UserVO.class);
// 5.封装返回
return new PageDTO<UserVO>(page.getTotal(), page.getPages(), list);
}
启动项目,在页面查看:
通用分页实体
- 之前的代码中
queryUsersPage
函数中构建分页条件这一部分和业务没有太大关系,应该是通用的,而且麻烦,任何一个业务都要编写这部分代码显然不合理,应该封装一下,作为一个工具,实现在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象。 - 之前的代码中
queryUsersPage
函数中拿到返回值结果后的代码,和业务也没有太大关系,也应该封装,实现在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果。
需求也是上面问题的解决方案:
- 在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
- 在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
我们完全可以在PageQuery这个实体中定义一个工具方法,简化开发。
package com.itheima.mp.domain.query;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
@Data
public class PageQuery {
private Integer pageNo;
private Integer 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;
}
public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}
public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
return toMpPage("create_time", false);
}
public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
return toMpPage("update_time", false);
}
}
这样我们在开发也时就可以省去对从PageQuery
到Page
的的转换:
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
//这里的query是UserQuery 是 PageQuery的子类,可以调用父类的方法
在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。
我们完全可以将其封装到PageDTO的工具方法中,简化整个过程。
package com.itheima.mp.domain.dto;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
private Long total;
private Long pages;
private List<V> list;
/**
* 返回空分页结果
* @param p MybatisPlus的分页结果
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> empty(Page<P> p){
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将MybatisPlus分页结果转为 VO分页结果
* @param p MybatisPlus的分页结果
* @param voClass 目标VO类型的字节码
* @param <V> 目标VO类型
* @param <P> 原始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.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = BeanUtil.copyToList(records, voClass);
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
/**
* 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
* @param p MybatisPlus的分页结果
* @param convertor PO到VO的转换函数
* @param <V> 目标VO类型
* @param <P> 原始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.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
}
最终,业务层的代码可以简化为:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, UserVO.class);
}
如果是希望自定义PO到VO的转换过程,可以这样做:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, user -> {
// 拷贝属性到VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 用户名脱敏
String username = vo.getUsername();
vo.setUsername(username.substring(0, username.length() - 2) + "**");
return vo;
});
}