目前独立负责一个单独的产品服务,技术栈是用的SpringBoot2 + MyBatis,分页用的MyBatis-PageHelper插件,版本是5.1.8
在项目中有一个非常复杂的查询语句,是用作模糊查询的,拢共LEFT JOIN了5张表,因为参数很多,又涉及到aggregation聚合操作,所以在MyBatis的xml里写了很多分支和逻辑操作。分页还是采用原始的PageHelper.start(from, to)
这种方式。
先说下这个分页原理,其实就是MyBatis在做真正的Query之前有一个interceptor,会先把要执行的查询语句做一个包装变成:
SELECT COUNT(0) FROM
(要执行的查询语句) table_count
先取到总数,然后再根据limit和offset做分页。
开始以为很简单,但奇怪的是分页居然出了问题,具体的表现就是,比如说一页取40个对象,有时候就会出现一页只有20多个,但总数和分页的个数是对的。稳定复现的问题可能很好定位,但时有时无的问题就很难解决。首先看下有没有人遇到过类似的问题,所以先试查阅MyBatis-Pagehelper官方文档,发现有类似的分页问题issue提到需要升级版本,从5.1.8再升级到5.1.10发现并没有任何变化。
这时候没发现有人遇到这种相似问题后的做法一般都是梳理下思路,从数据库的层面先排查下问题,把MyBatis执行的SQL打印出来,然后在数据库里执行,但发现有个有意思的现象就是比如从数据库里查询到有40条结果,这是分页后的结果,但页面上确只有38条,仔细比对后发现最后几条数据的id是有重复的,这是因为做了多次join,并且在resultmap中存在collection这种nested的嵌套型数据结构,不考虑分页的话,MyBatis自己会按照定义好的resultMap和结果集进行组合返回我们期望的数据结构。
所以结论结论就是MyBatis-Pagehelper这个插件如果用数据库的分页去实现分页的话,对于collection这种一对多或多对多的关系映射其实是没有办法实现分页的,最简单的分页办法就是全部取出来,然后放到List中取subList,最后再自己封装一个PageInfo对象即可,我也是这么做的。
封装的代码我先贴到这里: 其中users这个List就是一个subList,fromIndex是起始index,toIndex是截取到的结束index,total是所有user的总数,pageNumber是用户看到第几页,pageSize是一页有多少User
public class PageInfoBuilder {
public static PageInfo<User> build(List<User> users, int fromIndex, int toIndex, int total, int pageNumber, int pageSize) {
if (fromIndex < 0 || fromIndex >= users.size() || toIndex <= 0 || toIndex < fromIndex) {
return PageInfo.of(Collections.emptyList());
}
PageInfo<User> pageInfo = PageInfo.of(users.subList(fromIndex, toIndex));
pageInfo.setPageNum(pageNumber);
pageInfo.setPageSize(pageSize);
pageInfo.setTotal(total);
int totalPages = (int) Math.ceil((double) total / pageSize);
int [] navigatepageNums = new int[totalPages];
for (int i = 0; i < totalPages; i++) {
navigatepageNums[i] = i + 1;
}
pageInfo.setStartRow(fromIndex);
pageInfo.setEndRow(toIndex);
pageInfo.setNavigatePages(totalPages);
pageInfo.setNavigatepageNums(navigatepageNums);
pageInfo.setPages(totalPages);
calcNavigatepageNums(pageInfo, navigatepageNums);
calcPage(pageInfo, pageNumber, totalPages);
judgePageBoudary(pageInfo, pageNumber, totalPages);
return pageInfo;
}
private static void calcNavigatepageNums(PageInfo<Template> templatePageInfo, int[] navigatepageNums) {
if (navigatepageNums.length > 0) {
templatePageInfo.setNavigateFirstPage(navigatepageNums[0]);
templatePageInfo.setNavigateLastPage(navigatepageNums[navigatepageNums.length - 1]);
}
}
private static void calcPage(PageInfo<Template> templatePageInfo, int pageNumber, int totalPages) {
if (pageNumber > 1) {
templatePageInfo.setPrePage(pageNumber - 1);
}
if (pageNumber < totalPages) {
templatePageInfo.setNextPage(pageNumber + 1);
}
}
private static void judgePageBoudary(PageInfo<Template> templatePageInfo, int pageNumber, int totalPages) {
templatePageInfo.setIsFirstPage(pageNumber == 1);
templatePageInfo.setIsLastPage(pageNumber == totalPages || totalPages == 0);
templatePageInfo.setHasPreviousPage(pageNumber > 1);
templatePageInfo.setHasNextPage(pageNumber < totalPages);
}
}
这样处理完后分页就正常了,考虑到目前的搜索数据集并不是很大,所以可以考虑这种全部load到内存然后取subList的做法,但如果很大的话,比如几百万的用户的话这种做法可能就不是很合适,需要另寻解决方案。
最后在处理完问题后,在官方的wiki里有这么句话,嵌套的数据映射是不支持分页的,捂脸。
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Important.md