目前獨立負責一個單獨的產(chǎn)品服務(wù),技術(shù)棧是用的SpringBoot2 + MyBatis,分頁用的MyBatis-PageHelper插件,版本是5.1.8
在項目中有一個非常復雜的查詢語句,是用作模糊查詢的,攏共LEFT JOIN了5張表,因為參數(shù)很多,又涉及到aggregation聚合操作,所以在MyBatis的xml里寫了很多分支和邏輯操作。分頁還是采用原始的PageHelper.start(from, to)這種方式。
先說下這個分頁原理,其實就是MyBatis在做真正的Query之前有一個interceptor,會先把要執(zhí)行的查詢語句做一個包裝變成:
SELECT COUNT(0) FROM
(要執(zhí)行的查詢語句) table_count
先取到總數(shù),然后再根據(jù)limit和offset做分頁。
開始以為很簡單,但奇怪的是分頁居然出了問題,具體的表現(xiàn)就是,比如說一頁取40個對象,有時候就會出現(xiàn)一頁只有20多個,但總數(shù)和分頁的個數(shù)是對的。穩(wěn)定復現(xiàn)的問題可能很好定位,但時有時無的問題就很難解決。首先看下有沒有人遇到過類似的問題,所以先試查閱MyBatis-Pagehelper官方文檔,發(fā)現(xiàn)有類似的分頁問題issue提到需要升級版本,從5.1.8再升級到5.1.10發(fā)現(xiàn)并沒有任何變化。
這時候沒發(fā)現(xiàn)有人遇到這種相似問題后的做法一般都是梳理下思路,從數(shù)據(jù)庫的層面先排查下問題,把MyBatis執(zhí)行的SQL打印出來,然后在數(shù)據(jù)庫里執(zhí)行,但發(fā)現(xiàn)有個有意思的現(xiàn)象就是比如從數(shù)據(jù)庫里查詢到有40條結(jié)果,這是分頁后的結(jié)果,但頁面上確只有38條,仔細比對后發(fā)現(xiàn)最后幾條數(shù)據(jù)的id是有重復的,這是因為做了多次join,并且在resultmap中存在collection這種nested的嵌套型數(shù)據(jù)結(jié)構(gòu),不考慮分頁的話,MyBatis自己會按照定義好的resultMap和結(jié)果集進行組合返回我們期望的數(shù)據(jù)結(jié)構(gòu)。
所以結(jié)論結(jié)論就是MyBatis-Pagehelper這個插件如果用數(shù)據(jù)庫的分頁去實現(xiàn)分頁的話,對于collection這種一對多或多對多的關(guān)系映射其實是沒有辦法實現(xiàn)分頁的,最簡單的分頁辦法就是全部取出來,然后放到List中取subList,最后再自己封裝一個PageInfo對象即可,我也是這么做的。
封裝的代碼我先貼到這里: 其中users這個List就是一個subList,fromIndex是起始index,toIndex是截取到的結(jié)束index,total是所有user的總數(shù),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);
}
}
這樣處理完后分頁就正常了,考慮到目前的搜索數(shù)據(jù)集并不是很大,所以可以考慮這種全部load到內(nèi)存然后取subList的做法,但如果很大的話,比如幾百萬的用戶的話這種做法可能就不是很合適,需要另尋解決方案。
最后在處理完問題后,在官方的wiki里有這么句話,嵌套的數(shù)據(jù)映射是不支持分頁的,捂臉。
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/Important.md