原來PageHelper還有這個坑!
哈嘍,大家好,我是了不起。
Mybatis插件PageHelper很多人都會使用吧,有一次Copy的時(shí)候忘了去掉PageHelper.startPaeg()方法,結(jié)果導(dǎo)致數(shù)據(jù)查不到,debug 的時(shí)候發(fā)現(xiàn)第一次查詢沒有數(shù)據(jù),再查詢一次就有數(shù)據(jù)了,這個坑竟然也花了一點(diǎn)時(shí)間,那么今天就把這個坑填平。
這里我們一起看看其原理以及日常使用注意事項(xiàng)。
如何使用PageHelper
PageHelper是Mybatis-Plus中的一個插件,主要用于實(shí)現(xiàn)數(shù)據(jù)庫的分頁查詢功能。其核心原理是將傳入的頁碼和條數(shù)賦值給一個Page對象,并保存到本地線程ThreadLocal中。接下來,PageHelper會進(jìn)入Mybatis的攔截器環(huán)節(jié),在攔截器中獲取并處理剛才保存在ThreadLocal中的分頁參數(shù)。這些分頁參數(shù)會與原本的SQL語句和內(nèi)部已經(jīng)定義好的SQL進(jìn)行拼接,從而完成帶有分頁處理的SQL語句的構(gòu)建。
Pagehelper 有很多種使用方式,下面列出官方給出的幾種使用方式。
//第一種,RowBounds方式的調(diào)用
List<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二種,Mapper接口方式的調(diào)用,推薦這種使用方式。
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第三種,Mapper接口方式的調(diào)用,推薦這種使用方式。
PageHelper.offsetPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第四種,參數(shù)方法調(diào)用
//存在以下 Mapper 接口方法,你不需要在 xml 處理后兩個參數(shù)
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代碼中直接調(diào)用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);
//第五種,參數(shù)對象
//如果 pageNum 和 pageSize 存在于 User 對象中,只要參數(shù)有值,也會被分頁
//有如下 User 對象
public class User {
//其他fields
//下面兩個參數(shù)名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 處理后兩個參數(shù)
public interface CountryMapper {
List<Country> selectByPageNumSize(User user);
}
//當(dāng) user 中的 pageNum!= null && pageSize!= null 時(shí),會自動分頁
List<Country> list = countryMapper.selectByPageNumSize(user);
//第六種,ISelect 接口方式
//jdk6,7用法,創(chuàng)建接口
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//對應(yīng)的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> countryMapper.selectGroupBy());
//count查詢,返回一個查詢語句的count數(shù)
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectLike(country);
}
});
//lambda
total = PageHelper.count(()->countryMapper.selectLike(country));
PageHelper原理
其核心原理是將傳入的頁碼和條數(shù)賦值給一個Page對象,并保存到本地線程ThreadLocal中。
下面以常見的使用方式看一下:
PageHelper.startPage(1, 10, orderBy);
經(jīng)過一些列的循環(huán)俄羅斯套娃調(diào)用之后,來到了這里:
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//當(dāng)已經(jīng)執(zhí)行過orderBy的時(shí)候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
- 重點(diǎn)在setLocalPage(page) 這個方法,將page對象放到了靜態(tài)變量ThreadLocal中。
圖片
- 查詢接口進(jìn)來的時(shí)候, PageHelper中的Threadlocal對象中就保存的該線程對應(yīng)的分頁參數(shù),在調(diào)用查詢的時(shí)候就會拿出來使用。
PageHelper 中有寫了一個 com.github.pagehelper.PageInterceptor , 這里是執(zhí)行分頁的地方。如果你想要寫其他的攔截器,也可以自定義一個mybatis plugin包攔截器,在這里對sql進(jìn)行處理。
圖片
- 在PageInterceptor 中有一個主要interceptor方法,在方法中需要判斷是否需要分頁,如果需要分頁,則獲取分頁信息,查詢數(shù)據(jù)量等。
圖片
- 接下來,PageHelper會進(jìn)入Mybatis的攔截器環(huán)節(jié),在攔截器中獲取并處理剛才保存在ThreadLocal中的分頁參數(shù)。這些分頁參數(shù)會與原本的SQL語句和內(nèi)部已經(jīng)定義好的SQL進(jìn)行拼接,從而完成帶有分頁處理的SQL語句的構(gòu)建。需要注意一點(diǎn)的是在finally中remove掉 ThreadLocal對象中當(dāng)前線程的page對象。
圖片
- 調(diào)用skip方法, 并獲取分頁參數(shù) 判斷是否需要分頁。
圖片
- 從靜態(tài)ThreadLocal中獲取page對象,
圖片
PageHelper在執(zhí)行這一過程時(shí),會判斷SQL的類型,只有當(dāng)該SQL是查詢操作時(shí),才會進(jìn)入分頁邏輯。并且,在進(jìn)入分頁邏輯處理后,PageHelper會通過反射獲取該方法的參數(shù),判斷是否存在IPage對象(這是Mybatis-Plus中的另一個分頁對象)的實(shí)現(xiàn)類。如果存在這樣的實(shí)現(xiàn)類,那么也會進(jìn)行相應(yīng)的分頁處理。
PageHelper注意事項(xiàng)
使用PageHelper分頁推薦使用 PageHelper.startPage(1, 10) 這種方式。
另外startPage語句最好緊挨著查詢語句,避免中間拋出異常,沒有辦法清除ThreadLocal中當(dāng)前線程的page對象。
下面看一下官方給出的不安全的用法 和推薦的例子:
不安全用法:下面的方法如果不走查詢的話,page對象就會保留在當(dāng)前線程中,算是一個內(nèi)存泄漏,如果下一個mybatis查詢方法剛好是這個線程的時(shí)候,就會被動分頁。
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
推薦用法,主要是緊挨著mybatis查詢方法,查詢結(jié)束后,page對象就被清理,對線程后續(xù)的運(yùn)行不受影響。
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
回到最開始的那個坑,原因就在于錯誤的使用了PageHelper.startPage(page,size), 這里的分頁參數(shù)只是調(diào)用下游系統(tǒng)的分頁參數(shù),不應(yīng)該再這里使用,所以在第一頁的時(shí)候是沒問題的,到第二頁的時(shí)候,查詢就出問題了,數(shù)據(jù)庫中沒有那么多數(shù)據(jù),所以會查不到數(shù)據(jù)。 debug的時(shí)候,執(zhí)行一次數(shù)據(jù)查詢操作,把page對象消耗掉了,第二次查詢的數(shù)據(jù)沒有分頁參數(shù),所以就可以正常查詢出數(shù)據(jù)。