MybatisPlus分页查询详解
一直对于分页查询的插件用的不是很熟练,这次在学习mp的时候又一次学到了分页查询,在这个过程中发现学到的东西挺多的,想着可以分享给大家,往下看前请保证对泛型以及函数式接口编程有一定了解
1 MybatisPlus的基础介绍
这边主要是讲解他的一个分页功能因此基础的用法并不会很详细(可以自行学习)
1.1 配置环境
-
引入相关依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>
-
连接数据库
spring: datasource: url: jdbc:mysql://10.0.0.232:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: 数据库登录账号 password: 你数据库密码
1.2 核心功能
-
Mapper
为了简化单表CRUD,MybatisPlus提供了一个基础的
BaseMapper
接口,其中已经实现了单表的CRUD因此我们自定义的Mapper只要实现了这个
BaseMapper
,就无需自己实现单表CRUD了。public interface UserMapper extends BaseMapper<User> { }
-
新建一个测试类,编写几个单元测试,测试基本的CRUD功能:
@SpringBootTest class UserMapperTest { @Autowired private UserMapper userMapper; @Test void testInsert() { User user = new User(); user.setId(5L); user.setUsername("Lucy"); user.setPassword("123"); user.setPhone("18688990011"); user.setBalance(200); user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}"); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); userMapper.insert(user); } @Test void testSelectById() { User user = userMapper.selectById(5L); System.out.println("user = " + user); } @Test void testSelectByIds() { List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L, 5L)); users.forEach(System.out::println); } @Test void testUpdateById() { User user = new User(); user.setId(5L); user.setBalance(20000); userMapper.updateById(user); } @Test void testDelete() { userMapper.deleteById(5L); } }
-
条件构造器Wrapper
对于一些简单的CRUD操作MP都提供了现成的方法可以直接调用,但是稍微复杂些的带where的就需要用到条件构造器rapper
在我看来所谓的Wrapper其实就是将Sql语句中 where 后的的条件进行了封装以便于我们将进行简洁的代码开发,我们可以在上图看到继承结构于集合类似其实我们最终使用的还是最底层的实现类,由其名字就可知操作上分为查询和更新,其中又有lambda表达式的写法和正常构造的写法
这里就给两个简单是示例便于理解
-
QueryWrapper
@Test查询出名字中带o的,存款大于等于1000元的人 void testQueryWrapper() { // 1.构建查询条件 where name like "%o%" AND balance >= 1000 QueryWrapper<User> wrapper = new QueryWrapper<User>() .select("id", "username", "info", "balance") .like("username", "o") .ge("balance", 1000); // 2.查询数据 List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
-
UpdateWrapper
@Test更新id为1,2,4的用户的余额,扣200 void testUpdateWrapper() { List<Long> ids = List.of(1L, 2L, 4L); // 1.生成SQL UpdateWrapper<User> wrapper = new UpdateWrapper<User>() .setSql("balance = balance - 200") // SET balance = balance - 200 .in("id", ids); // WHERE id in (1, 2, 4) // 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据, // 而是基于UpdateWrapper中的setSQL来更新 userMapper.update(null, wrapper); }
显然这种写法固定了字段名称,在编程中是不推荐 因此还是推荐使用基于Lambda的Wrapper
@Test void testLambdaQueryWrapper() { // 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.lambda() .select(User::getId, User::getUsername, User::getInfo, User::getBalance) .like(User::getUsername, "o") .ge(User::getBalance, 1000); // 2.查询 List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
不难看出本质上就是基于变量的
gettter
方法结合反射技术得到对应字段传递给MP.对于以上代码中的userMapper 都是继承自MP给我们提供的BaseMapper 里面封装了许多基本CRUD
-
-
Service接口
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。 通用接口为
IService
,默认实现为ServiceImpl
-
基本用法
由于
Service
中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService
,而是自定义Service
接口,然后继承IService
以拓展方法。同时,让自定义的Service实现类
继承ServiceImpl
,这样就不用自己实现IService
中的接口了。首先,定义
IUserService
,继承IService
:public interface IUserService extends IService<User> { // 拓展自定义方法 }
然后,编写
UserServiceImpl
类,继承ServiceImpl
,实现UserService
:@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { }
其中
UserMapper
为svc要调用的mapper,User
为操作的实体类这里我们直接介绍最为常用的Lambda形式
Service中对
LambdaQueryWrapper
和LambdaUpdateWrapper
的用法进一步做了简化我们无需自己通过new
的方式来创建Wrapper
,而是直接调用lambdaQuery
和lambdaUpdate
方法:可以构造一个wrapper,还需要在链式编程的最后添加一个想要得到的结果===============================lambdaQuery @GetMapping("/list") @ApiOperation("根据id集合查询用户") public List<UserVO> queryUsers(UserQuery query){ // 1.组织条件 String username = query.getName(); Integer status = query.getStatus(); Integer minBalance = query.getMinBalance(); Integer maxBalance = query.getMaxBalance(); // 2.查询用户 List<User> users = userService.lambdaQuery() .like(username != null, User::getUsername, username) .eq(status != null, User::getStatus, status) .ge(minBalance != null, User::getBalance, minBalance) .le(maxBalance != null, User::getBalance, maxBalance) .list();//这里就是将结果转换为list集合 // 3.处理vo return BeanUtil.copyToList(users, UserVO.class); } ===============================lambdaUpdate=============================== @Override @Transactional public void deductBalance(Long id, Integer money) { // 1.查询用户 User user = getById(id); // 2.校验用户状态 if (user == null || user.getStatus() == 2) { throw new RuntimeException("用户状态异常!"); } // 3.校验余额是否充足 if (user.getBalance() < money) { throw new RuntimeException("用户余额不足!"); } // 4.扣减余额 update tb_user set balance = balance - ? int remainBalance = user.getBalance() - money; lambdaUpdate() .set(User::getBalance, remainBalance) // 更新余额 .set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status .eq(User::getId, id) .eq(User::getBalance, user.getBalance()) // 乐观锁 .update(); }
ok 通过以上的介绍相信大家已经可以正式的开始学习分页插件的用法啦!
-
2 插件功能
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor
:自动分页TenantLineInnerInterceptor
:多租户DynamicTableNameInnerInterceptor
:动态表名OptimisticLockerInnerInterceptor
:乐观锁IllegalSQLInnerInterceptor
:sql 性能规范BlockAttackInnerInterceptor
:防止全表更新与删除
这里我们通过学习分页插件来学习插件的用法
2.1 分页插件
在我们没有引入插件的情况下,默认是不支持分页功能的
IService
和BaseMapper
中的分页方法都无法正常起效。 所以,我们必须配置分页插件。
-
配置分页插件
这里我们创建一个配置类 并且添加分页插件
@Configuration public class MybatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { // 初始化核心插件 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
其实可以看到本质上就是创建了一个对sql语句的拦截器
-
基本使用
@Test void testPageQuery() { // 1.分页查询,new Page()的两个参数分别是:页码、每页大小 Page<User> p = userService.page(new Page<>(2, 2)); // 2.总条数 System.out.println("total = " + p.getTotal()); // 3.总页数 System.out.println("pages = " + p.getPages()); // 4.数据 List<User> records = p.getRecords(); records.forEach(System.out::println); }
-
接下来我们创建一个通用的分页实体
现在要实现一个分页查询的接口,接口规范如下:
参数 说明 请求方式 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 } ] }
通用的开发规范中,在这里我们需要定义三个实体类用于接受数据实体,返回数据实体以及一个分页结果实体
UserQuery
:分页查询条件的实体,包含分页,排序参数,过滤条件PageDTO
:分页结果实体,包含总条数、总页数、当前页数据UserVO
:用户页面视图实体
-
创建实体
由于分页查询这个功能不仅仅在某一张表上需要使用,因此我们可以将分页,排序参数单独定义一个通用类,其他需要查询的表实体进行继承即可这里我们首先创建一个PageQuery类包含参数
下方的
@ApiModel
@ApiModelProperty
注解是为了swagger接口测试时易读@Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty("页码") private Long pageNo; @ApiModelProperty("页码") private Long pageSize; @ApiModelProperty("排序字段") private String sortBy; @ApiModelProperty("是否升序") private Boolean isAsc; }
这里我们针对用户表进行分页,因此使用用户表继承
@Data @EqualsAndHashCode @ApiModel(description = "用户查询条件实体") public class UserQuery extends PageQuery{ @ApiModelProperty("用户名关键字") private String name; @ApiModelProperty("用户状态:1-正常,2-冻结") private Integer status; }
接下来我们创建
UserVO
用户页面视图实体@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; }
最后我们创建分页结果实体
@Data public class PageDTO<UserVO> { @ApiModelProperty("总条数") private Long total; @ApiModelProperty("总页数") private Long pages; @ApiModelProperty("集合") private List<UserVO> list; }
实体创建完成 ,接下来我们正式开发接口
-
开发接口
controller层
@RestController @RequestMapping("users") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/page") public PageDTO<UserVO> queryUsersPage(UserQuery query){ return userService.queryUsersPage(query); } }
svc 以及svcImpl
PageDTO<UserVO> queryUsersPage(PageQuery query);
@Override public PageDTO<UserVO> queryUsersPage(PageQuery query) { // 1.构建条件 // 过滤条件 String name = query.getName(); Integer status = query.getStatus(); // 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<User> userPage = lambdaQuery() .like(name != null, User::getUsername, name) .eq(status != null, User::getStatus, status) .page(page); // 3.数据非空校验 List<User> records = userpage.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); }
总的步骤可以分为:
- 构建相关条件
- 通过分页条件构造分页
- 判断排序条件是否存在
- 查询
- 数据的非空校验
- 数据的转换
- 返回
这里我们可以看到整个过程其实是相对比较繁琐的,我们可以注意到其实就是从请求参数query–>pageDTO转换的前3个步骤以及从page–>pageDTO转换的后3个步骤都是相对固定的,因此我们可以将其抽象到相关的实体类中简化代码的编写过程!
-
对于从query–>page
我们可以改写PageQuery
@Data public class PageQuery { private Integer pageNo = 1; private Integer pageSize = 5; private String sortBy; private Boolean isAsc = true; public <T> Page<T> toMpPage(OrderItem ... orderItems) { //1 构建分页条件 Page<T> page = Page.of(pageNo, pageSize); // 2 排序条件 // 2.1 判断前端是否传入排序字段 if(sortBy != null) { page.addOrder(new OrderItem(sortBy, isAsc)); return page; } //2.2 检查是否手动传入排序字段 if( orderItems != null && orderItems.length > 0 ) { page.addOrder(orderItems); return page; } return page; } //直接指定 public <T>Page<T> toMpPage(String orderItem,boolean isAsc) { return this.toMpPage(orderItem,isAsc); } public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() { return toMpPage("create_time", false); } public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() { return toMpPage("update_time", false); } }
可以注意到这里我们定义了一个泛型T,这是因为我们其实并不知道使用分页查询时每一页的数据类型是啥,而使用泛型T就可以通用化
这样我们的quer–>page 就可以简化成
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
-
对于page–>pageDTO
我们改造pageDTO
@Data public class PageDTO<T> { @ApiModelProperty("总条数") private Long total; @ApiModelProperty("总页数") private Long pages; @ApiModelProperty("集合") private List<T> list; /** * 对po到vo的转换 * @param userPage * @param clazz * @return * @param <PO> * @param <VO> */ public static <PO,VO> PageDTO<VO> of(Page<PO> userPage , Class<VO> clazz){ // 封装VO结果 PageDTO<VO> pageDTO = new PageDTO<>(); pageDTO.setTotal(userPage.getTotal()); pageDTO.setPages(userPage.getPages()); List<PO> list = userPage.getRecords(); //判断数据为空 if(CollUtil.isNotEmpty(list)){ pageDTO.setList(Collections.emptyList()); return pageDTO; } //PO转VO List<VO> userVOList = BeanUtil.copyToList(list, clazz); pageDTO.setList(userVOList); //4 返回 return pageDTO; } }
这里同样定义泛型 这个转换其实本质上就是一个PO–>VO的转换因此我们为了通用化可以定义<PO,VO>两个泛型
但是这样就要求PO和VO中要转换的字段名一致,不可以进行特殊的转换,我们可以重载函数并且利用函数式接口编程定义一个转换器进行特定转换
/** * 利用函数式接口编程创建一个转换器可以特定转换 * @param userPage * @param convert * @return * @param <PO> * @param <VO> */ public static <PO,VO> PageDTO<VO> of(Page<PO> userPage , Function<PO,VO> convert){ //3 封装VO结果 PageDTO<VO> pageDTO = new PageDTO<>(); pageDTO.setTotal(userPage.getTotal()); pageDTO.setPages(userPage.getPages()); List<PO> list = userPage.getRecords(); //判断数据为空 if(CollUtil.isNotEmpty(list)){ pageDTO.setList(Collections.emptyList()); return pageDTO; } //PO转VO List<VO> userVOList = list.stream().map(convert).collect(Collectors.toList()); pageDTO.setList(userVOList); //4 返回 return pageDTO; }
这里利用到了stream流使用map转换,这样我们就可以在调用处编写特定的转换逻辑
最终实体为
@Data public class PageDTO<T> { @ApiModelProperty("总条数") private Long total; @ApiModelProperty("总页数") private Long pages; @ApiModelProperty("集合") private List<T> list; /** * 对po到vo的转换 * @param userPage * @param clazz * @return * @param <PO> * @param <VO> */ public static <PO,VO> PageDTO<VO> of(Page<PO> userPage , Class<VO> clazz){ //3 封装VO结果 PageDTO<VO> pageDTO = new PageDTO<>(); pageDTO.setTotal(userPage.getTotal()); pageDTO.setPages(userPage.getPages()); List<PO> list = userPage.getRecords(); //判断数据为空 if(CollUtil.isNotEmpty(list)){ pageDTO.setList(Collections.emptyList()); return pageDTO; } //PO转VO List<VO> userVOList = BeanUtil.copyToList(list, clazz); pageDTO.setList(userVOList); //4 返回 return pageDTO; } /** * 利用函数式接口编程创建一个转换器可以特定转换 * @param userPage * @param convert * @return * @param <PO> * @param <VO> */ public static <PO,VO> PageDTO<VO> of(Page<PO> userPage , Function<PO,VO> convert){ //3 封装VO结果 PageDTO<VO> pageDTO = new PageDTO<>(); pageDTO.setTotal(userPage.getTotal()); pageDTO.setPages(userPage.getPages()); List<PO> list = userPage.getRecords(); //判断数据为空 if(CollUtil.isNotEmpty(list)){ pageDTO.setList(Collections.emptyList()); return pageDTO; } //PO转VO List<VO> userVOList = list.stream().map(convert).collect(Collectors.toList()); pageDTO.setList(userVOList); //4 返回 return pageDTO; } }
最终我们的代码可以简化为如下
@Override public PageDTO<UserVO> queryUsersByPage(UserQuery query) { // 过滤条件 String name = query.getName(); Integer status = query.getStatus(); // 分页条件 Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc(); // 分页查询` Page<User> userPage = lambdaQuery() .like(name != null, User::getUsername, name) .eq(status != null, User::getStatus, status) .page(page); //返回 // return PageDTO.of(userPage, UserVO.class); 无需进行特定的处理 return PageDTO.of(userPage,user -> { UserVO userVO = BeanUtil.copyProperties(user, UserVO.class); //可以进行特定处理如(将后2个名字隐藏) userVO.setUsername(userVO.getUsername().substring(0,userVO.getUsername().length() - 2) + "**"); return userVO; }); }
3 总结
可以看到使用MP其实更多的式将我们在mapper层操作的过程隐藏起来了,便于我们更好的进行开发,我认为本篇最有价值的还是这里的通用化思想以及函数式接口编程的运用