Github PageHelper 原理解析

来源:https://www.cnblogs.com/gkmeteor/archive/2019/11/20/11900984.html
-Advertisement-
Play Games

任何服務對資料庫的日常操作,都離不開增刪改查。如果一次查詢的紀錄很多,那我們必須採用分頁的方式。對於一個Springboot項目,訪問和查詢MySQL資料庫,持久化框架可以使用MyBatis,分頁工具可以使用github的 PageHelper。我們來看一下PageHelper的使用方法: 1 // ...


任何服務對資料庫的日常操作,都離不開增刪改查。如果一次查詢的紀錄很多,那我們必須採用分頁的方式。對於一個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方法: