日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Github PageHelper 原理解析

發布時間:2025/3/12 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Github PageHelper 原理解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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

// 組裝查詢條件 ArticleVO articleVO = new ArticleVO(); articleVO.setAuthor("張三");// 初始化返回類 // ResponsePages類是這樣一種返回類,其中包括返回代碼code和返回消息msg // 還包括返回的數據和分頁信息 // 其中,分頁信息就是 com.github.pagehelper.Page<?> 類型 ResponsePages<List<ArticleVO>> responsePages = new ResponsePages<>();// 這里為了簡單,寫死分頁參數。正確的做法是從查詢條件中獲取 // 假設需要獲取第1頁的數據,每頁20條記錄 // com.github.pagehelper.Page<?> 類的基本字段如下 // pageNum: 當前頁 // pageSize: 每頁條數 // total: 總記錄數 // pages: 總頁數 com.github.pagehelper.Page<?> page = PageHelper.startPage(1, 20);// 根據條件獲取文章列表 List<ArticleVO> articleList = articleMapper.getArticleListByCondition(articleVO);// 設置返回數據 responsePages.setData(articleList);// 設置分頁信息 responsePages.setPage(page);

如代碼所示,page 是組裝好的分頁參數,即每頁顯示20條記錄,并且顯示第1頁。然后我們執行mapper的獲取文章列表的方法,返回了結果。此時我們查看 responsePages 的內容,可以看到 articleList 中有20條記錄,page中包括當前頁,每頁條數,總記錄數,總頁數等信息。

使用方法就是這么簡單,但是僅僅知道如何使用還不夠,還需要對原理有所了解。下面就來看看,PageHelper 實現分頁的原理。

我們先來看看 startPage 方法。進入此方法,發現一堆方法重載,最后進入真正的 startPage 方法,有5個參數,如下所示:

/*** 開始分頁** @param pageNum 頁碼* @param pageSize 每頁顯示數量* @param count 是否進行count查詢* @param reasonable 分頁合理化,null時用默認配置* @param pageSizeZero true 且 pageSize=0 時返回全部結果,false時分頁, null時用默認配置*/ 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);// 當已經執行過orderBy的時候Page<E> oldPage = SqlUtil.getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}SqlUtil.setLocalPage(page);return page; }

getLocalPage 和 setLocalPage 方法做了什么操作?我們進入基類 BaseSqlUtil 看一下:

package com.github.pagehelper.util; ...public class BaseSqlUtil {// 省略其他代碼private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();/*** 從 ThreadLocal<Page> 中獲取 page*/public static <T> Page<T> getLocalPage() {return LOCAL_PAGE.get();}/*** 將 page 設置到 ThreadLocal<Page>*/public static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}// 省略其他代碼 }

原來是將 page 放入了 ThreadLocal 中。ThreadLocal 是每個線程獨有的變量,與其他線程不影響,是放置 page 的好地方。

setLocalPage 之后,一定有地方 getLocalPage,我們跟蹤進入代碼來看。

有了MyBatis動態代理的知識后,我們知道最終執行SQL的地方是 MapperMethod 的 execute 方法,作為回顧,我們來看一下:

package org.apache.ibatis.binding; ...public class MapperMethod {public Object execute(SqlSession sqlSession, Object[] args) {Object result;if (SqlCommandType.INSERT == command.getType()) {// 省略} else if (SqlCommandType.UPDATE == command.getType()) {// 省略} else if (SqlCommandType.DELETE == command.getType()) {// 省略} else if (SqlCommandType.SELECT == command.getType()) {if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {/*** 獲取多條記錄*/result = executeForMany(sqlSession, args);} else if ...// 省略} else if (SqlCommandType.FLUSH == command.getType()) {// 省略} else {throw new BindingException("Unknown execution method for: " + command.getName());}...return result;} }

由于執行的是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 中的重要信息sqlSessionFactoryconfigurationenvironment mapperRegistryconfig 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方法:

package org.apache.ibatis.plugin; ...public class Plugin implements InvocationHandler {...public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 執行攔截器的邏輯return interceptor.intercept(new Invocation(target, method, args));}return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}... }

我們去看 intercept 方法的實現,這里我們進入【PageHelper】類來看:

package com.github.pagehelper; .../*** Mybatis - 通用分頁攔截器*/ @SuppressWarnings("rawtypes") @Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public class PageHelper extends BasePageHelper implements Interceptor {private final SqlUtil sqlUtil = new SqlUtil();@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 執行 sqlUtil 的攔截邏輯return sqlUtil.intercept(invocation);}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {sqlUtil.setProperties(properties);} }

可以看到最終調用了 SqlUtil 的intercept 方法,里面的 doIntercept 方法是 PageHelper 原理中最重要的方法。跟進來看:

package com.github.pagehelper.util; ...public class SqlUtil extends BaseSqlUtil implements Constant {.../*** 真正的攔截器方法** @param invocation* @return* @throws Throwable*/public Object intercept(Invocation invocation) throws Throwable {try {return doIntercept(invocation); // 執行攔截} finally {clearLocalPage(); // 清空 ThreadLocal<Page>}}/*** 真正的攔截器方法** @param invocation* @return* @throws Throwable*/public Object doIntercept(Invocation invocation) throws Throwable {// 省略其他代碼// 調用方法判斷是否需要進行分頁if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {ResultHandler resultHandler = (ResultHandler) args[3];// 當前的目標對象Executor executor = (Executor) invocation.getTarget();/*** getBoundSql 方法執行后,boundSql 中保存的是沒有 limit 的sql語句*/BoundSql boundSql = ms.getBoundSql(parameterObject);// 反射獲取動態參數Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);// 判斷是否需要進行 count 查詢,默認需要if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {// 省略代碼// 執行 count 查詢Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);Long count = (Long) ((List) countResultList).get(0);// 處理查詢總數,從 ThreadLocal<Page> 中取出 page 并設置 totalruntimeDialect.afterCount(count, parameterObject, rowBounds);if (count == 0L) {// 當查詢總數為 0 時,直接返回空的結果return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);}}// 判斷是否需要進行分頁查詢if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {/*** 生成分頁的緩存 key* pageKey變量是分頁參數存放的地方*/CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);/*** 處理參數對象,會從 ThreadLocal<Page> 中將分頁參數取出來,放入 pageKey 中* 主要邏輯就是這樣,代碼就不再單獨貼出來了,有興趣的同學可以跟進驗證*/parameterObject = runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);/*** 調用方言獲取分頁 sql* 該方法執行后,pageSql中保存的sql語句,被加上了 limit 語句*/String pageSql = runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);//設置動態參數for (String key : additionalParameters.keySet()) {pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));}/*** 執行分頁查詢*/resultList = executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);} else {resultList = new ArrayList();}} else {args[2] = RowBounds.DEFAULT;// 不需要分頁查詢,執行原方法,不走代理resultList = (List) invocation.proceed();}/*** 主要邏輯:* 從 ThreadLocal<Page> 中取出 page* 將 resultList 塞進 page,并返回*/return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);}... }

Count 查詢語句 countBoundSql 被執行了,分頁查詢語句 pageBoundSql 也被執行了。然后從 ThreadLocal 中將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,讓業務代碼可以獲取。

    總結

    以上是生活随笔為你收集整理的Github PageHelper 原理解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。