任何服務對數據庫的日常操作,都離不開增刪改查。如果一次查詢的紀錄很多,那我們必須採用分頁的方式。對於一個Springboot項目,訪問和查詢MySQL數據庫,持久化框架可以使用MyBatis,分頁工具可以使用github的 PageHelper。我們來看一下PageHelper的使用方法:
1 // 組裝查詢條件
2 ArticleVO articleVO = new ArticleVO();
3 articleVO.setAuthor("劉慈欣");
4
5 // 初始化返回類
6 // ResponsePages類是這樣一種返回類,其中包括返回代碼code和返回消息msg
7 // 還包括返回的數據和分頁信息
8 // 其中,分頁信息就是 com.github.pagehelper.Page<?> 類型
9 ResponsePages<List<ArticleVO>> responsePages = new ResponsePages<>();
10
11 // 這裏為了簡單,寫死分頁參數。正確的做法是從查詢條件中獲取
12 // 假設需要獲取第1頁的數據,每頁20條記錄
13 // com.github.pagehelper.Page<?> 類的基本字段如下
14 // pageNum: 當前頁
15 // pageSize: 每頁條數
16 // total: 總記錄數
17 // pages: 總頁數
18 com.github.pagehelper.Page<?> page = PageHelper.startPage(1, 20);
19
20 // 根據條件獲取文章列表
21 List<ArticleVO> articleList = articleMapper.getArticleListByCondition(articleVO);
22
23 // 設置返回數據
24 responsePages.setData(articleList);
25
26 // 設置分頁信息
27 responsePages.setPage(page);
如代碼所示,page 是組裝好的分頁參數,即每頁显示20條記錄,並且显示第1頁。然後我們執行mapper的獲取文章列表的方法,返回了結果。此時我們查看 responsePages 的內容,可以看到 articleList 中有20條記錄,page中包括當前頁,每頁條數,總記錄數,總頁數等信息。 使用方法就是這麼簡單,但是僅僅知道如何使用還不夠,還需要對原理有所了解。下面就來看看,PageHelper 實現分頁的原理。 我們先來看看 startPage 方法。進入此方法,發現一堆方法重載,最後進入真正的 startPage 方法,有5個參數,如下所示:
1 /**
2 * 開始分頁
3 *
4 * @param pageNum 頁碼
5 * @param pageSize 每頁显示數量
6 * @param count 是否進行count查詢
7 * @param reasonable 分頁合理化,null時用默認配置
8 * @param pageSizeZero true 且 pageSize=0 時返回全部結果,false時分頁, null時用默認配置
9 */
10 public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
11 Page<E> page = new Page<E>(pageNum, pageSize, count);
12 page.setReasonable(reasonable);
13 page.setPageSizeZero(pageSizeZero);
14 // 當已經執行過orderBy的時候
15 Page<E> oldPage = SqlUtil.getLocalPage();
16 if (oldPage != null && oldPage.isOrderByOnly()) {
17 page.setOrderBy(oldPage.getOrderBy());
18 }
19 SqlUtil.setLocalPage(page);
20 return page;
21 }
getLocalPage 和 setLocalPage 方法做了什麼操作?我們進入基類 BaseSqlUtil 看一下:
1 package com.github.pagehelper.util;
2 ...
3
4 public class BaseSqlUtil {
5 // 省略其他代碼
6
7 private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
8
9 /**
10 * 從 ThreadLocal<Page> 中獲取 page
11 */
12 public static <T> Page<T> getLocalPage() {
13 return LOCAL_PAGE.get();
14 }
15
16 /**
17 * 將 page 設置到 ThreadLocal<Page>
18 */
19 public static void setLocalPage(Page page) {
20 LOCAL_PAGE.set(page);
21 }
22
23 // 省略其他代碼
24 }
原來是將 page 放入了 ThreadLocal<Page> 中。ThreadLocal 是每個線程獨有的變量,與其他線程不影響,是放置 page 的好地方。 setLocalPage 之後,一定有地方 getLocalPage,我們跟蹤進入代碼來看。 有了MyBatis動態代理的知識后,我們知道最終執行SQL的地方是 MapperMethod 的 execute 方法,作為回顧,我們來看一下:
1 package org.apache.ibatis.binding;
2 ...
3
4 public class MapperMethod {
5
6 public Object execute(SqlSession sqlSession, Object[] args) {
7 Object result;
8 if (SqlCommandType.INSERT == command.getType()) {
9 // 省略
10 } else if (SqlCommandType.UPDATE == command.getType()) {
11 // 省略
12 } else if (SqlCommandType.DELETE == command.getType()) {
13 // 省略
14 } else if (SqlCommandType.SELECT == command.getType()) {
15 if (method.returnsVoid() && method.hasResultHandler()) {
16 executeWithResultHandler(sqlSession, args);
17 result = null;
18 } else if (method.returnsMany()) {
19 /**
20 * 獲取多條記錄
21 */
22 result = executeForMany(sqlSession, args);
23 } else if ...
24 // 省略
25 } else if (SqlCommandType.FLUSH == command.getType()) {
26 // 省略
27 } else {
28 throw new BindingException("Unknown execution method for: " + command.getName());
29 }
30 ...
31
32 return result;
33 }
34 }
由於執行的是select操作,並且需要查詢多條紀錄,所以我們進入 executeForMany 這個方法中,然後進入 selectList 方法,然後是 executor.query 方法。再然後突然進入到了 mybatis 的 Plugin 類的 invoke 方法,這是為什麼? 這裏就必須提到 mybatis 提供的 Interceptor 接口。
Intercept 機制讓我們可以將自己製作的分頁插件 intercept 到查詢語句執行的地方,這是MyBatis對外提供的標準接口。藉助於Java的動態代理,標準的攔截器可以攔截在指定的數據庫訪問流程中,執行攔截器自定義的邏輯,比如在執行SQL之前攔截,拼裝一個分頁的SQL並執行。 讓我們回到MyBatis初始化的時候,我們發現 MyBatis 為我們組裝了 sqlSessionFactory,所有的 sqlSession 都是生成自這個 Factory。在這篇文章中,我們將重點放在 interceptorChain 上。程序啟動時,MyBatis 或者是 mybatis-spring 會掃描代碼中所有實現了 interceptor 接口的插件,並將它們以【攔截器集合】的方式,存儲在 interceptorChain 中。如下所示:
# sqlSessionFactory 中的重要信息
sqlSessionFactory
configuration
environment
mapperRegistry
config
knownMappers
mappedStatements
resultMaps
sqlFragments
interceptorChain # MyBatis攔截器調用鏈
interceptors # 攔截器集合,記錄了所有實現了Interceptor接口,並且使用了invocation變量的類
如果MyBatis檢測到有攔截器,它就會在攔截器指定的執行點,首先執行 Plugin 的 invoke 方法,喚醒攔截器,然後執行攔截器定義的邏輯。因此,當 query 方法即將執行的時候,其實執行的是攔截器的邏輯。 MyBatis官網的說明: MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用。默認情況下,MyBatis 允許使用插件來攔截的方法調用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
如果想了解更多攔截器的知識,可以看文末的參考資料。 我們回到主線,繼續看Plugin類的invoke方法:
1 package org.apache.ibatis.plugin;
2 ...
3
4 public class Plugin implements InvocationHandler {
5 ...
6
7 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
8 try {
9 Set<Method> methods = signatureMap.get(method.getDeclaringClass());
10 if (methods != null && methods.contains(method)) {
11 // 執行攔截器的邏輯
12 return interceptor.intercept(new Invocation(target, method, args));
13 }
14 return method.invoke(target, args);
15 } catch (Exception e) {
16 throw ExceptionUtil.unwrapThrowable(e);
17 }
18 }
19 ...
20 }
我們去看 intercept 方法的實現,這裏我們進入【PageHelper】類來看:
1 package com.github.pagehelper;
2 ...
3
4 /**
5 * Mybatis - 通用分頁攔截器
6 */
7 @SuppressWarnings("rawtypes")
8 @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
9 public class PageHelper extends BasePageHelper implements Interceptor {
10 private final SqlUtil sqlUtil = new SqlUtil();
11
12 @Override
13 public Object intercept(Invocation invocation) throws Throwable {
14 // 執行 sqlUtil 的攔截邏輯
15 return sqlUtil.intercept(invocation);
16 }
17
18 @Override
19 public Object plugin(Object target) {
20 return Plugin.wrap(target, this);
21 }
22
23 @Override
24 public void setProperties(Properties properties) {
25 sqlUtil.setProperties(properties);
26 }
27 }
可以看到最終調用了 SqlUtil 的intercept 方法,裏面的 doIntercept 方法是 PageHelper 原理中最重要的方法。跟進來看:
1 package com.github.pagehelper.util;
2 ...
3
4 public class SqlUtil extends BaseSqlUtil implements Constant {
5 ...
6
7 /**
8 * 真正的攔截器方法
9 *
10 * @param invocation
11 * @return
12 * @throws Throwable
13 */
14 public Object intercept(Invocation invocation) throws Throwable {
15 try {
16 return doIntercept(invocation); // 執行攔截
17 } finally {
18 clearLocalPage(); // 清空 ThreadLocal<Page>
19 }
20 }
21
22 /**
23 * 真正的攔截器方法
24 *
25 * @param invocation
26 * @return
27 * @throws Throwable
28 */
29 public Object doIntercept(Invocation invocation) throws Throwable {
30 // 省略其他代碼
31
32 // 調用方法判斷是否需要進行分頁
33 if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {
34 ResultHandler resultHandler = (ResultHandler) args[3];
35 // 當前的目標對象
36 Executor executor = (Executor) invocation.getTarget();
37
38 /**
39 * getBoundSql 方法執行后,boundSql 中保存的是沒有 limit 的sql語句
40 */
41 BoundSql boundSql = ms.getBoundSql(parameterObject);
42
43 // 反射獲取動態參數
44 Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
45 // 判斷是否需要進行 count 查詢,默認需要
46 if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {
47 // 省略代碼
48
49 // 執行 count 查詢
50 Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
51 Long count = (Long) ((List) countResultList).get(0);
52
53 // 處理查詢總數,從 ThreadLocal<Page> 中取出 page 並設置 total
54 runtimeDialect.afterCount(count, parameterObject, rowBounds);
55 if (count == 0L) {
56 // 當查詢總數為 0 時,直接返回空的結果
57 return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);
58 }
59 }
60 // 判斷是否需要進行分頁查詢
61 if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {
62 /**
63 * 生成分頁的緩存 key
64 * pageKey變量是分頁參數存放的地方
65 */
66 CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
67 /**
68 * 處理參數對象,會從 ThreadLocal<Page> 中將分頁參數取出來,放入 pageKey 中
69 * 主要邏輯就是這樣,代碼就不再單獨貼出來了,有興趣的同學可以跟進驗證
70 */
71 parameterObject = runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);
72 /**
73 * 調用方言獲取分頁 sql
74 * 該方法執行后,pageSql中保存的sql語句,被加上了 limit 語句
75 */
76 String pageSql = runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
77 BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
78 //設置動態參數
79 for (String key : additionalParameters.keySet()) {
80 pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
81 }
82 /**
83 * 執行分頁查詢
84 */
85 resultList = executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
86 } else {
87 resultList = new ArrayList();
88 }
89 } else {
90 args[2] = RowBounds.DEFAULT;
91 // 不需要分頁查詢,執行原方法,不走代理
92 resultList = (List) invocation.proceed();
93 }
94 /**
95 * 主要邏輯:
96 * 從 ThreadLocal<Page> 中取出 page
97 * 將 resultList 塞進 page,並返回
98 */
99 return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);
100 }
101 ...
102 }
Count 查詢語句 countBoundSql 被執行了,分頁查詢語句 pageBoundSql 也被執行了。然後從 ThreadLocal<Page> 中將page 取出來,設置記錄總數,每頁條數等信息,同時也將查詢到的記錄塞進page,最後返回。再之後就是mybatis的常規後續操作了。
知識拓展
我們來看看 PageHelper 支持哪些數據庫的分頁操作:
- Oracle
- Mysql
- MariaDB
- SQLite
- Hsqldb
- PostgreSQL
- DB2
- SqlServer(2005,2008)
- Informix
- H2
- SqlServer2012
- Derby
- Phoenix
原來 PageHelper 支持這麼多數據庫,那麼持久化工具mybatis為什麼不一口氣把分頁也做了呢? 其實mybatis也有自帶的分頁方法:RowBounds。
RowBounds簡單地來說包括 offset 和 limit。實現原理是將所有符合條件的記錄獲取出來,然後丟棄 offset 之前的數據,只獲取 limit 條數據。這種做法效率低下,個人猜想mybatis只想把數據庫連接和SQL執行這方面做精做強,至於如分頁之類的細節,本身提供Intercept接口,讓第三方實現該接口來完成分頁。PageHelper 就是這樣的第三方分頁插件。甚至你可以實現該接口,製作你自己的業務邏輯,攔截到任何MyBatis允許你攔截的地方。
總結
PageHelper 的分頁原理,最核心的部分是實現了 MyBatis 的 Interceptor 接口,從而將分頁參數攔截在執行sql之前,拼裝出分頁sql到數據庫中執行。 初始化的時候,因為 PageHelper 的 SqlUtil 中實例化了 intercept 方法,因此MyBatis 將它視作一個攔截器,記錄在 interceptorChain 中。 執行的時候,PageHelper首先將 page 需求記錄在 ThreadLocal<Page> 中,然後在攔截的時候,從 ThreadLocal<Page> 中取出 page,拼裝出分頁sql,然後執行。 同時將結果分頁信息(包括當前頁,每頁條數,總頁數,總記錄數等)設置回page,讓業務代碼可以獲取。
參考資料
- PageHelper淺析:
- MyBatis攔截器:
- ThreadLocal理解:
創作時間:2019-11-20 21:21
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益
※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象
※台灣寄大陸海運貨物規則及重量限制?
※大陸寄台灣海運費用試算一覽表
※台中搬家,彰化搬家,南投搬家前需注意的眉眉角角,別等搬了再說!