Mybatis-PageHelper分頁插件的使用與相關(guān)原理分析

前言

今天使用了分頁插件,并將其整合到SpringBoot中。各種遇到了個別問題,現(xiàn)在記錄下。吃一墊長一智。

整合

與SpringBoot整合

1. 引入依賴

   <!--pagehelper 分頁插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>

2. 配置參數(shù)

接著在application.yml中配置相關(guān)參數(shù)

#pagehelper
pagehelper:
    helperDialect: mysql
    reasonable: true
    supportMethodsArguments: true
    params: count=countSql
    returnPageInfo: check

參數(shù)說明
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md

3.使用

  #方式1
  PageHelper.startPage(1, 10);
   List<ScoreGoodsCategory> goodsCategoryList = mapper.selectByPage();
   int totalCount=(int) ((Page)goodsCategoryList).getTotal();
   # 方式二
   PageHelper.offsetPage(1, 10);
   List<ScoreGoodsCategory> goodsCategoryList = mapper.selectByPage();
   PageInfo<ScoreGoodsCategory> pageInfo = new PageInfo<>(goodsCategoryList);
   int totalCount=(int) pageInfo.getTotal();

與Spring MVC 整合

1. 引入依賴

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>最新版本</version>
</dependency>

2. 配置攔截器(這是核心,如果不配置則分頁不起作用)

在Spring的配置文件中配置攔截器插件

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <!-- 注意其他配置 -->
  <property name="plugins">
    <array>
      <bean class="com.github.pagehelper.PageInterceptor">
        <property name="properties">
          <!--使用下面的方式配置參數(shù),一行配置一個 -->
          <value>
            params=count=countSql
          </value>
		   <value>
            helperDialect=mysql
          </value>
		   <value>
            reasonable=true
          </value>
		    <value>
            supportMethodsArguments=true
          </value>
		     <value>
            returnPageInfo=check
          </value>
        </property>
      </bean>
    </array>
  </property>
</bean>

配置好之后,使用同上。

原理

其最核心的方法就在攔截器中,那我們首先看看攔截器中的攔截方法。該方法主要做了兩件事,1. 統(tǒng)計總條數(shù),2.對原始的SQL進行改寫使其可以分頁。
PageInterceptor類的intercept方法是攔截器的總?cè)肟诜椒ā?/p>

1.統(tǒng)計總條數(shù)

首先,我們來看看統(tǒng)計總條數(shù)的相關(guān)代碼。

//PageInterceptor 類
//設(shè)置count的sql的id,在原始的msId后面加上_COUNT后綴。
 String countMsId = msId + countSuffix;
 // 生成統(tǒng)計sql的入口方法
 count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);

接下來我們就來看看executeAutoCount 方法

  /**
     * 執(zhí)行自動生成的 count 查詢
     *
     * @param executor sql執(zhí)行器
     * @param countMs
     * @param parameter
     * @param boundSql 
     * @param rowBounds  
     * @param resultHandler  結(jié)果處理器
     * @return
     * @throws IllegalAccessException
     * @throws SQLException
     */
    private Long executeAutoCount(Executor executor, MappedStatement countMs,
                                   Object parameter, BoundSql boundSql,
                                   RowBounds rowBounds, ResultHandler resultHandler) throws IllegalAccessException, SQLException {
        Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
        //創(chuàng)建 count 查詢的緩存 key
        CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
        //調(diào)用方言獲取 count sql
        String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
        //countKey.update(countSql);
        BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
        //當使用動態(tài) SQL 時,可能會產(chǎn)生臨時的參數(shù),這些參數(shù)需要手動設(shè)置到新的 BoundSql 中
        for (String key : additionalParameters.keySet()) {
            countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
        //執(zhí)行 count 查詢
        Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
        Long count = (Long) ((List) countResultList).get(0);
        return count;
    }

如上,方法的注釋比較詳實,此處我們主要介紹下第二步調(diào)用方言獲取 count sql。dialect 是一個接口類,PageHelper是其的一個實現(xiàn)類。接著我們來看看PageHelper中的getCountSql方法。

//*PageHelper
    @Override
    public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
        return autoDialect.getDelegate().getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);
    }

如上,在PageHelper的getCountSql直接把請求給了AbstractHelperDialect(通過autoDialect.getDelegate() 獲得) 的getCountSql 方法。我們接著往下看

//*AbstractHelperDialect
    @Override
    public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
        Page<Object> page = getLocalPage();
        String countColumn = page.getCountColumn();
        if (StringUtil.isNotEmpty(countColumn)) {
            return countSqlParser.getSmartCountSql(boundSql.getSql(), countColumn);
        }
        return countSqlParser.getSmartCountSql(boundSql.getSql());
    }

如上,countsql語句的生成邏輯最終落在了CountSqlParser類,該類是一個通用的sql解析類。最后我們來看看CountSqlParser類的getSmartCountSql方法。

 /**
     * 獲取智能的countSql
     *
     * @param sql 我們傳入的需要分頁的sql
     * @param name 列名,默認 0
     * @return
     */
    public String getSmartCountSql(String sql, String name) {
        //解析SQL
        Statement stmt = null;
        //特殊sql不需要去掉order by時,使用注釋前綴
        if(sql.indexOf(KEEP_ORDERBY) >= 0){
            return getSimpleCountSql(sql);
        }
        try {
		// 對sql 進行分解
            stmt = CCJSqlParserUtil.parse(sql);
        } catch (Throwable e) {
            //無法解析的用一般方法返回count語句
            return getSimpleCountSql(sql);
        }
        Select select = (Select) stmt;
        SelectBody selectBody = select.getSelectBody();
        try {
            //處理body-去order by
            processSelectBody(selectBody);
        } catch (Exception e) {
            //當 sql 包含 group by 時,不去除 order by
            return getSimpleCountSql(sql);
        }
        //處理with-去order by
        processWithItemsList(select.getWithItemsList());
        //處理為count查詢
        sqlToCount(select, name);
        String result = select.toString();
        return result;
    }

調(diào)試結(jié)果:

大致流程如下(舉例說明):

  1. 原始sql: Select id, name FROM student WHERE sex=?
  2. 分解sql存入SelectBody的實現(xiàn)類PlainSelect中,主要的部分是selectItems,fromItem,where
  3. 然后就是將selectItems替換成count(0)
  4. 最后在組裝成sql返回,組裝后的sql是Select count(0) FROM student WHERE sex=?
    countsql的生成邏輯說完之后,接下來我們看看分頁過程。

2. 對sql進行分頁

對sql 進行分頁的入口邏輯還是在PageInterceptor類的intercept方法中。話不多說,上代碼。


 //生成分頁的緩存 key
                    CacheKey pageKey = cacheKey;
                    //處理參數(shù)對象
                    parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
                    //調(diào)用方言獲取分頁 sql
                    String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
                    BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
                    //設(shè)置動態(tài)參數(shù)
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //執(zhí)行分頁查詢
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);

如上,主要四步:

  1. 處理參數(shù)對象
  2. 獲取待分頁的sql,
  3. 設(shè)置動態(tài)參數(shù),
  4. 執(zhí)行分頁查詢。

部分問題處理(踩坑)

  1. 坑一、在查出數(shù)據(jù)集合list之后對list做了處理,例如:
        List<String> taxNoList = juhePayClearingRecordMapper.pageClearingTaxNo(tradeTime);
        if (taxNoList == null) {
            return null;
        }
        taxNoList = taxNoList.stream().filter(b -> StringUtils.isNotBlank(b)).collect(Collectors.toList());
        PageInfo<String> pageInfo = new PageInfo<>(taxNoList);
        PagePOJO<String> taxNoInfo = new PagePOJO<>();
        taxNoInfo.setACount((int) pageInfo.getTotal());

如上,先通過SQL語句插入集合taxNoList,然后對taxNoList做了一個去除空字符串的處理,再獲取總條數(shù)時,數(shù)據(jù)不對
這是因為查詢出來的taxNoList,實際上是一個Page對象,所以獲取總條數(shù)是調(diào)用的((Page)list).getTotal()來獲取的。而如果對taxNoList進行處理之后,他就變成了一個普通的ArrayList對象了。所以,獲取的總條數(shù)total不對。
在這里插入圖片描述
2. 坑二、在PageHelper.startPage 方法之后添加了代碼,同樣的會導(dǎo)致不能分頁
在這里插入圖片描述

總結(jié)

首先感謝liuzh同志開發(fā)出了這款好用的插件,代碼很規(guī)范,插件很好用。本文首先介紹了Mybatis-PageHelper插件的整合與使用,接著介紹了相關(guān)原理,主要是統(tǒng)計總條數(shù)的實現(xiàn)原理。希望對讀者朋友們有所幫助。

https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md





作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥