一、前言
作为一个 CRUD 工程师,查询必然少不了,分页查询更是常见,市面上也有很多成熟的分页插件,都各有优缺点,这里整理一下,基于 MybatisPlus 的分页插件进一步封装分页的公共方法。
二、对象封装
其实分页插件已经提供了很强大的功能,但是在业务开发的时候不够精简,返回了很多我们并不关注的数据,在这个基础上进一步封装,使其更贴合我们的业务开发。
2.1 分页结果对象封装
首先我们定义一个通用的分页结果对象,PageVO 包含我们关注的主要几个数据值,总条数,总页数,数据集。这里为了兼容各种数据类型,这里的数据集的类型通过泛型指定
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageVO<V> implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "总条数")
private Long total;
@Schema(description = "总页数")
private Long pages;
@Schema(description = "数据")
private List<V> records;
}
2.2 分页查询对象封装
为了兼容查询对象的不同类型,这里使用泛型定义查询对象类型,后面我们只需要根据使用场景定义对应的 Query 对象就可以了
@Data
public class PageQuery<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "当前页码", defaultValue = "1")
private Integer pageNum = 1;
@Schema(description = "每页显示条数", defaultValue = "10")
private Integer pageSize = 10;
@Schema(description = "排序对象,支持多字段排序")
private List<OrderItem> orderItems;
@Schema(description = "查询对象")
private T search;
}
2.3 结合 Query 对象使用案例
第一步:
比如我们现在要完成用户列表的分页查询,那么首先我们需要定义对应的查询对象 **UserQuery, **这里简单展示通过用户名和昵称进行查询。
@Data
public class UserQuery implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
}
第二步:
定义我们返回时需要的结果对象,我这里就叫 **UserListVO **我习惯将列表的 **VO **对象命名为 **xxxListVO,**详情对象命名为 xxxDetailVO
@Data
public class UserListVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
private Long userId;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
第三步:
在 controller 层编写接口
@Operation(summary = "分页查询")
@PostMapping("/page")
public R<PageVO<UserListVO>> findPage(@RequestBody PageQuery<UserQuery> userQuery) {
PageVO<UserListVO> page = userService.findPage(userQuery);
return R.ok(page);
}
可以看到这里我们通过前面定义的公共对象,以及具体的业务对象,经过简单的组装完成了,请求参数 **userQuery **以及返会结果的封装,并且我们可以很清楚的知道对应的类型,想要扩展也很容易实现,以后所有的分页查询基本上都是类似的格式,不同的在于我们根据不同使用场景封装对应的业务返回 xxxVO 以及查询对象 xxxQuery
第四步:
具体的分页查询实现,即 findPage 方法的实现
@Override
public PageVO<UserListVO> findPage(PageQuery<UserQuery> userQuery) {
// 将查询对象 转换为 Mybatis Plus 的 Page 对象
Page<AdminUser> page = Page.of(userQuery.getPageNum(), userQuery.getPageSize());
UserQuery search = userQuery.getSearch();
// 查询
lambdaQuery()
.eq(StrUtil.isNotBlank(search.getUsername()), AdminUser::getUsername, search.getUsername())
.or()
.like(StrUtil.isNotBlank(search.getNickname()), AdminUser::getNickname, search.getNickname())
.page(page);
// 将 Mybatis Plus 的 Page 对象 转换为 PageVO
List<AdminUser> records = page.getRecords();
List<UserListVO> userListVOs = BeanUtil.copyToList(records, UserListVO.class);
return new PageVO<>(page.getTotal(), page.getPages(), userListVOs);
}
测试一下
到这里基本上已经完成了,但是细心的会发现我们没有处理排序字段,而且这种对象来回转换的方法非常繁琐。
三、进一步封装对象转换
对象转换处理:
基于上面的接口实现进一步完善,首先第一点,查询对象 转换为 Mybatis Plus 的 Page 对象,我们先来完成这个封装。
你可以单独写到一个工具类里,这里我直接写在 PageQuery 对象中,这里方便我拿取参数,省的传参了,而且这样也更符合面向对象编程,这种转换能力应该属于 PageQuery 对象。
/**
* 将当前对象转换为 MybatisPlus 分页对象
*
* @param <PO> PO类型
* @return Page<PO>
*/
public <PO> Page<PO> toMpPage() {
return Page.of(pageNum, pageSize);
}
那相同的 VO的转换能力应该由 PageVO提供,所以 VO转换写在 PageVO 里
/**
* 将 MybatisPlus 分页结果转换为 PageDTO
*
* @param page MybatisPlus 分页结果
* @param targetClass 目标类型字节码
* @param <V> 目标数据类型
* @param <P> 原始数据类型
* @return 分页结果 PageDTO
*/
public static <V, P> PageVO<V> of(Page<P> page, Class<V> targetClass) {
List<P> records = page.getRecords();
if (records.isEmpty()) {
return empty(page);
}
// 将原始数据转换为目标数据 这里我使用了 hutool 的 BeanUtil,可以根据需要自行替换
List<V> vs = BeanUtil.copyToList(records, targetClass);
return new PageVO<>(page.getTotal(), page.getPages(), vs);
}
/**
* 返回空的分页结果
*
* @param page MybatisPlus 分页结果
* @param <V> 目标数据类型
* @param <P> 原始数据类型
* @return 分页结果 PageDTO
*/
public static <V, P> PageVO<V> empty(Page<P> page) {
return new PageVO<>(page.getPages(), page.getPages(), Collections.emptyList());
}
这样我们之前的分页查询就可以写成这样
@Override
public PageVO<UserListVO> findPage(PageQuery<UserQuery> userQuery) {
// 将查询对象 转换为 Mybatis Plus 的 Page 对象
Page<AdminUser> page = userQuery.toMpPage();
UserQuery search = userQuery.getSearch();
// 查询
lambdaQuery()
.eq(StrUtil.isNotBlank(search.getUsername()), AdminUser::getUsername, search.getUsername())
.or()
.like(StrUtil.isNotBlank(search.getNickname()), AdminUser::getNickname, search.getNickname())
.page(page);
// 将 Mybatis Plus 的 Page 对象 转换为 PageVO
return PageVO.of(page, UserListVO.class);
}
排序处理:
在我们处理将当前对象转换为 MybatisPlus分页对象的时候,只处理了 pageNum 和 pageSize , 接下来我们处理一下排序的情况。
/**
* 将当前对象转换为 MybatisPlus 分页对象
*
* @param <PO> PO类型
* @return Page<PO>
*/
public <PO> Page<PO> toMpPage() {
Page<PO> page = Page.of(pageNum, pageSize);
if (orderItems != null && !orderItems.isEmpty()) {
page.addOrder(orderItems);
} else {
// 如果不传默认根据创建时间倒序
page.addOrder(OrderItem.desc("create_time"));
}
return page;
}
测试一下
==> Preparing: SELECT user_id, username, password, nickname, create_time, update_time, is_deleted FROM itshare_admin_user WHERE is_deleted = 0 ORDER BY user_id DESC LIMIT ?
控制台输出的 SQL 也如我们预期一样
多条件测试
==> Preparing: SELECT user_id, username, password, nickname, create_time, update_time, is_deleted FROM itshare_admin_user WHERE is_deleted = 0 ORDER BY user_id DESC, create_time ASC LIMIT ?
四、总结
这样我们基本上完成了项目中分页场景下的代码封装,后续分页场景,我们只需要定义好 xxxQuery 对象,以及 xxxVO 对象即可完成分页查询,大大简化了编码过程,提高了编码效率。其实就目前我们依然有很多具有共性的代码,比如对条件 sql 的编写,我们能不能根据对象类型以及前端配合传参动态去实现,这样我们就可以完全解放双手,定义两个对象就搞定一个分页接口的查询了。这个后面我会再写一篇文章和大家一起探讨一下。