PageHelper 官網:
- https://pagehelper.github.io
- 關於PageHelper 的開發和原理官網上也已經講的很明確了,這裏不過多解析官網的意思
快速構建:
- 首先需要一個 SSM 項目【也可以單體Mybatis】
- 新增Maven 配置:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.10</version>
</dependency>
- mybatis-config.xml 加入以下配置【也可以寫在Spring的配置中,寫法略有不同】:
<configuration>
<!--Mysql 配置駝峯命名-->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!--分頁插件-->
<plugins>
<!-- com.github.pagehelper爲PageHelper類所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor" >
</plugin>
</plugins>
</configuration>
- Dao 以及 Xml 寫法。
// Dao
List<ClassInfo> getListByPage();
<!-- XML -->
<select id="getListByPage" parameterType="com.github.pagehelper.PageInfo" resultType="com.morning.all.entity.test.ClassInfo">
select * from tb_class
</select>
- 重點Spring 的實現類:
/**
* 分頁查詢數據
* @return
*/
public PageInfo getListByPage(PageInfo pageInfo){
//設置分頁信息(第幾頁,每頁數量),並將Page 對象放入到 ThreadLocal 中,
PageHelper.startPage(pageInfo.getPageNum(), pageInfo.getPageSize());
//查詢結果,我們的攔截器會在這裏起作用
List<ClassInfo> result = classDao.getListByPage();
// 生成返回結果
PageInfo<ClassInfo> pageInfoResult = new PageInfo<ClassInfo>(result);
return pageInfoResult;
}
這裏需要注意的是,getListByPage 的參數PageInfo 僅僅由於接受 前端分頁參數,不可用於返回。
- 通過Controller執行我們的代碼,看一下Mybatis sql打印結果:
哦吼,爲什麼會打印出這兩條SQL ,一條Count,一條Select,我原來的Sql 爲什麼沒有執行那,不着急我們看下一部分
源碼淺析:
- 要想真正理解這個功能,需要對Mybatis源碼 和 Mybatis攔截器有一定的瞭解,那麼PageInterceptor中究竟做了什麼那,直接上源碼,因爲它是中國人開發的,代碼註釋也一目瞭然,完全Copy
package com.github.pagehelper;
public class PageInterceptor implements Interceptor {
private volatile Dialect dialect;
private String countSuffix = "_COUNT";
protected Cache<String, MappedStatement> msCountMap = null;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由於邏輯關係,只會進入一次
if (args.length == 4) {
//4 個參數時
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個參數時
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
List resultList;
//調用方法判斷是否需要進行分頁,如果不需要,直接返回結果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判斷是否需要進行 count 查詢
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查詢總數
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//處理查詢總數,返回 true 時繼續分頁查詢,false 時直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當查詢總數爲 0 時,直接返回空的結果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用參數值,不使用分頁插件處理時,仍然支持默認的內存分頁
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}
/**
* Spring bean 方式配置時,如果沒有配置屬性就不會執行下面的 setProperties 方法,就不會初始化
* <p>
* 因此這裏會出現 null 的情況 fixed #26
*/
private void checkDialectExists() {
if (dialect == null) {
synchronized (default_dialect_class) {
if (dialect == null) {
setProperties(new Properties());
}
}
}
}
private Long count(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
String countMsId = ms.getId() + countSuffix;
Long count;
//先判斷是否存在手寫的 count 查詢
MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
if (countMs != null) {
count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自動創建
if (countMs == null) {
//根據當前的 ms 創建一個返回值爲 Long 類型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
return count;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
//緩存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
dialect.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
}
}
- Mybatis 執行 Dao層方法時執行時,會被攔截器攔截執行Interceptor.intercept()方法,我們可以再源碼中看到 第37行時查詢了總條數,在第44行執行了 分頁查詢,也就是說PageInterceptor會攔截請求,找到我們本來要執行的Sql並將其拆解,生成 count 和 Select Limit Sql 執行,並最後將結果返回。
- 那麼爲什麼我們原來的XML 配置的sql 並沒有執行那,是因爲Mybatis執行XML中的sql 也是一個攔截器,Mybatis執行攔截器是責任鏈的形式,而Mybatis自己的攔截器優先級最低,先執行了 PageInterceptor,並且PageInterceptor 中沒有調用 Mybatis本身的攔截器,所以就沒有執行。需要在PageInterceptor.intercept() 中加入代碼: Object result = invocation.proceed(); 程序纔會回去執行下一級攔截器,顯然這裏根本不需要執行下一個攔截器