1. 程式人生 > 實用技巧 >Mybatis分頁外掛: pageHelper的使用及其原理解析

Mybatis分頁外掛: pageHelper的使用及其原理解析

  在實際工作中,很進行列表查詢的場景,我們往往都需要做兩個步驟:1. 查詢所需頁數對應資料;2. 統計符合條件的資料總數;而這,又會導致我們必然至少要寫2個sql進行操作。這無形中增加了我們的工作量,另外,當發生需要變動時,我們又需要同時改動這兩個sql,否則必然導致結果的不一致。

  因此,我們需要一個簡單易用的分頁工具來幫我們完成這個工作了,需求明確,至於如何實現則各有千秋。而我們要說的 pageHelper則是這其中實現比較好的一件的元件了,我們就一起來看看如何使用它進行提升工作效率吧!

1. pageHelper 的依賴引入

  pom.xml 中引入pageHelper依賴:

  1. 如果是springboot, 則可以直接引入 pagehelper-spring-boot-starter, 它會幫我們省去許多不必要的配置。

        <!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version
>1.2.12</version> </dependency>

  2. 如果是普通的springmvc 類的專案,則引入 pageHelper 即可。

        <!-- pageHelper -->
        <dependency>
          <groupId>com.github.pagehelper</groupId>
          <artifactId>pagehelper</artifactId>
          <version
>5.1.10</version> </dependency>

2. pagehelper外掛配置

  1. 如果是springboot,則直接配置幾個配置項即可:

# mybatis 相關配置
mybatis:
  #... 其他配置資訊
  configuration-properties:
    offsetAsPageNum: true
    rowBoundsWithCount: true
    reasonable: true
  mapper-locations: mybatis/mapper/*.xml
 

  簡單回顧看下db配置:

# db 配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123
    url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&charactorEncoding=utf8&&serverTimezone=Asia/Shanghai

  2. 普通springmvc專案配置:mybatis-config.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
 
<configuration>
  <plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
      <!-- 該引數預設為false -->
      <!-- 設定為true時,會將RowBounds第一個引數offset當成pageNum頁碼使用 -->
      <!-- 和startPage中的pageNum效果一樣-->
      <property name="offsetAsPageNum" value="true"/>
      <!-- 該引數預設為false -->
      <!-- 設定為true時,使用RowBounds分頁會進行count查詢 -->
      <property name="rowBoundsWithCount" value="true"/>
      <!-- 設定為true時,如果pageSize=0或者RowBounds.limit = 0就會查詢出全部的結果 -->
      <!-- (相當於沒有執行分頁查詢,但是返回結果仍然是Page型別)-->
      <property name="pageSizeZero" value="true"/>
      <!-- 3.3.0版本可用 - 分頁引數合理化,預設false禁用 -->
      <!-- 啟用合理化時,如果pageNum<1會查詢第一頁,如果pageNum>pages會查詢最後一頁 -->
      <!-- 禁用合理化時,如果pageNum<1或pageNum>pages會返回空資料 -->
      <property name="reasonable" value="true"/>
      <!-- 3.5.0版本可用 - 為了支援startPage(Object params)方法 -->
      <!-- 增加了一個`params`引數來配置引數對映,用於從Map或ServletRequest中取值 -->
      <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置對映的用預設值 -->
      <!-- 不理解該含義的前提下,不要隨便複製該配置 -->
      <property name="params" value="pageNum=start;pageSize=limit;"/>
      <!-- 支援通過Mapper介面引數來傳遞分頁引數 -->
      <property name="supportMethodsArguments" value="true"/>
      <!-- always總是返回PageInfo型別,check檢查返回型別是否為PageInfo,none返回Page -->
      <property name="returnPageInfo" value="check"/>
    </plugin>
  </plugins>
</configuration>

  並在配置資料來源的時候,將mybatis配置檔案指向以上檔案。

3. pagehelper 的使用

  使用的時候,只需在查詢list前,呼叫 startPage 設定分頁資訊,即可使用分頁功能。

    public Object getUsers(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        // 不帶分頁的查詢
        List<UserEntity> list = userMapper.selectAllWithPage(null);
        // 可以將結果轉換為 Page , 然後獲取 count 和其他結果值
        com.github.pagehelper.Page listWithPage = (com.github.pagehelper.Page) list;
        System.out.println("listCnt:" + listWithPage.getTotal());
        return list;
    }

  即使用時, 只需提前宣告要分頁的資訊, 得到的結果就是有分頁資訊的了. 如果不想進行count, 只要查分頁資料, 則呼叫: PageHelper.startPage(pageNum, pageSize, false); 即可, 避免了不必要的count消耗.

4. pageHelper 實現原理1: interceptor

  mybatis 有個外掛機制,可以支援外部應用進行任意擴充套件。它在啟動的時候會將 interceptor 新增到mybatis的上下文中。然後在進行查詢時再觸發例項化動作.

4.1 springboot 中接入interceptor

  springboot 中接入pagehelper非常簡單, 主要受益於初始化的方式, 它會自動載入配置.

    // com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration#addPageInterceptor
    @PostConstruct
    public void addPageInterceptor() {
        // 初始化 com.github.pagehelper.PageInterceptor
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的屬性放進去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放進去,由於close-conn 利用上面方式時,屬性名就是 close-conn 而不是 closeConn,所以需要額外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            // 新增inteceptor到 mybatis 中
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }
  // org.apache.ibatis.session.Configuration#addInterceptor
  public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }
  
  // org.apache.ibatis.plugin.InterceptorChain#addInterceptor
  public void addInterceptor(Interceptor interceptor) {
    // 使用 ArrayList 儲存intceptor
    interceptors.add(interceptor);
  }

  藉助springboot的自動配置, 獲取mybatis的sqlSessionFactoryList, 依次將 pagehelper 接入其中。

4.2 interceptor的初始化

  將 interceptor 新增到mybatis上下文後, 會在每次呼叫查詢時進行攔截請求, 它的初始化也會在這時候觸發.

  // org.apache.ibatis.session.Configuration#newExecutor
  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 以interceptorChain包裝 executor, 以便inteceptor發揮作用
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
  
  // org.apache.ibatis.plugin.InterceptorChain#pluginAll
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // 使用plugin一層層包裝 target, 具體實現為使用代理包裝 target
      // 所以, interceptor 的使用順序是按照新增的順序來的, 並不能自行設定
      target = interceptor.plugin(target);
    }
    return target;
  }
  
    // com.github.pagehelper.PageInterceptor#plugin
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
  // org.apache.ibatis.plugin.Plugin#wrap
  public static Object wrap(Object target, Interceptor interceptor) {
    // 獲取註解中說明的方式列表 @Intercepts -> @Signature, 下面我們看 pageInterceptor的註解
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 過濾需要進行代理的介面, 而非全部代理
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // 使用jdk方式生成動態代理
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          // 使用 Plugin 包裝代理實現
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
  // pageInterceptor的註解, 即定義要攔截的方法列表
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
  // 過濾代理的介面
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        // 只有設定了的接口才會被新增
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

  這樣, interceptor 就和executor綁定了, 後續的查詢將會看到interceptor 的作用.

4.3 interceptor的呼叫過程

  在executor被代理後, 會繼續執行查詢動作, 這時就會被interceptor攔截了.

  // org.apache.ibatis.plugin.Plugin#invoke
  @Override
  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)) {
        // 匹配的方法會被攔截, 即 query 方法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
    // pageHelper 正式起作用的入口
    // com.github.pagehelper.PageInterceptor#intercept
    @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();
            }
        }
    }

  以上就是 pageHelper 的大體執行框架了:

    1. 先解析各位置引數;
    2. 初始化 pageHelper 例項, 即 dialect;
    3. 呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果;
    4. 判斷是否要進行count, 如果需要則實現一次count, ;
    5. 查詢分頁結果;
    6. 封裝帶分頁的結果返回;

  下面我們就每個細節依次看看實現吧.

4.4 是否跳過分頁判定

  首先會進行是否需要跳過分頁邏輯,如果跳過, 則直接執行mybatis的核心邏輯繼續查詢. 而是否要跳過分頁, 則是通過直接獲取page分頁引數來決定的,沒有分頁引數設定,則跳過, 否則執行分頁查詢. 這算是分頁的一個入口判定呢。

    /**
     * 跳過 count 和 分頁查詢
     *
     * @param ms              MappedStatement
     * @param parameterObject 方法引數
     * @param rowBounds       分頁引數
     * @return true 跳過,返回預設查詢結果,false 執行分頁查詢
     */
     // com.github.pagehelper.PageHelper#skip
    @Override
    public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        if (ms.getId().endsWith(MSUtils.COUNT)) {
            throw new RuntimeException("在系統中發現了多個分頁外掛,請檢查系統配置!");
        }
        // 如果 page 返回null, 則不需要進行分頁, 即是否呼叫  PageHelper.start(pageNo, pageSize) 方法
        Page page = pageParams.getPage(parameterObject, rowBounds);
        if (page == null) {
            return true;
        } else {
            //設定預設的 count 列
            if (StringUtil.isEmpty(page.getCountColumn())) {
                page.setCountColumn(pageParams.getCountColumn());
            }
            autoDialect.initDelegateDialect(ms);
            return false;
        }
    }
    // com.github.pagehelper.page.PageAutoDialect#initDelegateDialect
    //多資料動態獲取時,每次需要初始化
    public void initDelegateDialect(MappedStatement ms) {
        if (delegate == null) {
            if (autoDialect) {
                // 比如 MySqlDialect
                this.delegate = getDialect(ms);
            } else {
                dialectThreadLocal.set(getDialect(ms));
            }
        }
    }

    /**
     * 獲取分頁引數
     */
    // com.github.pagehelper.page.PageParams#getPage
    public Page getPage(Object parameterObject, RowBounds rowBounds) {
        Page page = PageHelper.getLocalPage();
        if (page == null) {
            if (rowBounds != RowBounds.DEFAULT) {
                if (offsetAsPageNum) {
                    page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
                } else {
                    page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
                    //offsetAsPageNum=false的時候,由於PageNum問題,不能使用reasonable,這裡會強制為false
                    page.setReasonable(false);
                }
                if(rowBounds instanceof PageRowBounds){
                    PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
                    page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
                }
            } else if(parameterObject instanceof IPage || supportMethodsArguments){
                try {
                    page = PageObjectUtil.getPageFromObject(parameterObject, false);
                } catch (Exception e) {
                    return null;
                }
            }
            if(page == null){
                return null;
            }
            PageHelper.setLocalPage(page);
        }
        //分頁合理化
        if (page.getReasonable() == null) {
            page.setReasonable(reasonable);
        }
        //當設定為true的時候,如果pagesize設定為0(或RowBounds的limit=0),就不執行分頁,返回全部結果
        if (page.getPageSizeZero() == null) {
            page.setPageSizeZero(pageSizeZero);
        }
        return page;
    }

  才上判定決定了後續的分頁效果,主要是利用 ThreadLocal 來儲存分頁資訊,從而與使用者程式碼產生關聯。

4.5 pageHelper 的 count 操作

  判斷是否是否需要count, 這些判定都會以 PageHelper 作為門面類進行接入, 而特殊地方則由具體方言實現.

    // com.github.pagehelper.PageHelper#beforeCount
    @Override
    public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);
    }

    // com.github.pagehelper.dialect.AbstractHelperDialect#beforeCount
    @Override
    public boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
        // 獲取page引數資訊, 該引數設定在 ThreadLocal 中
        Page page = getLocalPage();
        return !page.isOrderByOnly() && page.isCount();
    }
    // 如果需要進行count, 則需要自行組裝count邏輯進行查詢.
    // com.github.pagehelper.PageInterceptor#count
    private Long count(Executor executor, MappedStatement ms, Object parameter,
                       RowBounds rowBounds, ResultHandler resultHandler,
                       BoundSql boundSql) throws SQLException {
        // 在原有list 查詢後新增  _COUNT 代表count查詢id
        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;
    }
    // 建立count ms
    // com.github.pagehelper.util.MSUtils#newCountMappedStatement(org.apache.ibatis.mapping.MappedStatement, java.lang.String)
    public static MappedStatement newCountMappedStatement(MappedStatement ms, String newMsId) {
        // 直接基於原有 sql 構建新的 MappedStatement
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), newMsId, ms.getSqlSource(), ms.getSqlCommandType());
        builder.resource(ms.getResource());
        // 注意此處並未使用到使用者設定的分頁引數 
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        //count查詢返回值int
        List<ResultMap> resultMaps = new ArrayList<ResultMap>();
        ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Long.class, EMPTY_RESULTMAPPING).build();
        resultMaps.add(resultMap);
        builder.resultMaps(resultMaps);
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }

    /**
     * 執行自動生成的 count 查詢
     */
     // com.github.pagehelper.util.ExecutorUtil#executeAutoCount
    public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs,
                                        Object parameter, BoundSql boundSql,
                                        RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        //建立 count 查詢的快取 key
        CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
        //呼叫方言獲取 count sql
        String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
        //countKey.update(countSql);
        BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
        //當使用動態 SQL 時,可能會產生臨時的引數,這些引數需要手動設定到新的 BoundSql 中
        for (String key : additionalParameters.keySet()) {
            countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
        //執行 count 查詢
        Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
        Long count = (Long) ((List) countResultList).get(0);
        return count;
    }
    // com.github.pagehelper.PageHelper#getCountSql
    @Override
    public String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
        // 委託給各方言實現 sql 組裝
        return autoDialect.getDelegate().getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);
    }

    // com.github.pagehelper.dialect.AbstractHelperDialect#getCountSql
    @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
     *
     * @param sql
     * @param name 列名,預設 0
     * @return
     */
     // com.github.pagehelper.parser.CountSqlParser#getSmartCountSql(java.lang.String, java.lang.String)
    public String getSmartCountSql(String sql, String name) {
        //解析SQL
        Statement stmt = null;
        //特殊sql不需要去掉order by時,使用註釋字首
        if(sql.indexOf(KEEP_ORDERBY) >= 0){
            return getSimpleCountSql(sql, name);
        }
        try {
            stmt = CCJSqlParserUtil.parse(sql);
        } catch (Throwable e) {
            //無法解析的用一般方法返回count語句
            return getSimpleCountSql(sql, name);
        }
        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, name);
        }
        //處理with-去order by
        processWithItemsList(select.getWithItemsList());
        //處理為count查詢
        sqlToCount(select, name);
        String result = select.toString();
        return result;
    }
    /**
     * 將sql轉換為count查詢
     *
     * @param select
     */
     // com.github.pagehelper.parser.CountSqlParser#sqlToCount
    public void sqlToCount(Select select, String name) {
        SelectBody selectBody = select.getSelectBody();
        // 是否能簡化count查詢
        List<SelectItem> COUNT_ITEM = new ArrayList<SelectItem>();
        // 如 select * from user 將會被轉化為 select count(0) from user
        COUNT_ITEM.add(new SelectExpressionItem(new Column("count(" + name +")")));
        if (selectBody instanceof PlainSelect && isSimpleCount((PlainSelect) selectBody)) {
            // 簡單sql直接轉換select欄位為 count(0) 即可, 而這個sql是否支援這種方式則得仔細驗證
            ((PlainSelect) selectBody).setSelectItems(COUNT_ITEM);
        } else {
            // 如果對於複雜的sql查詢, 則只能在現有sql外圍加一個 select count(0) from (xxxxx) as table_count
            PlainSelect plainSelect = new PlainSelect();
            SubSelect subSelect = new SubSelect();
            subSelect.setSelectBody(selectBody);
            subSelect.setAlias(TABLE_ALIAS);
            // 將原sql作為臨時表放入 plainSelect 中
            plainSelect.setFromItem(subSelect);
            plainSelect.setSelectItems(COUNT_ITEM);
            // 替換原有 select
            select.setSelectBody(plainSelect);
        }
    }
    /**
     * 是否可以用簡單的count查詢方式
     */
     // net.sf.jsqlparser.statement.select.PlainSelect
    public boolean isSimpleCount(PlainSelect select) {
        //包含group by的時候不可以
        if (select.getGroupBy() != null) {
            return false;
        }
        //包含distinct的時候不可以
        if (select.getDistinct() != null) {
            return false;
        }
        for (SelectItem item : select.getSelectItems()) {
            //select列中包含引數的時候不可以,否則會引起引數個數錯誤
            if (item.toString().contains("?")) {
                return false;
            }
            //如果查詢列中包含函式,也不可以,函式可能會聚合列
            if (item instanceof SelectExpressionItem) {
                Expression expression = ((SelectExpressionItem) item).getExpression();
                if (expression instanceof Function) {
                    String name = ((Function) expression).getName();
                    if (name != null) {
                        String NAME = name.toUpperCase();
                        if(skipFunctions.contains(NAME)){
                            //go on
                        } else if(falseFunctions.contains(NAME)){
                            return false;
                        } else {
                            for (String aggregateFunction : AGGREGATE_FUNCTIONS) {
                                if(NAME.startsWith(aggregateFunction)){
                                    falseFunctions.add(NAME);
                                    return false;
                                }
                            }
                            skipFunctions.add(NAME);
                        }
                    }
                }
            }
        }
        return true;
    }

  大體上講就是分析sql, 如果是簡單查詢, 則直接將欄位內容轉換為 count(0) 即可, 這和我們普通認為的在select外部簡單包一層還不太一樣哦. 但是對於複雜查詢咱們還是隻能使用外包一層的實現方式了. 當然了,以上實現是針對mysql的,其他語言可能會有不一樣的實現.

4.6 select list 的改裝

  在執行完count後, 分頁的功能完成了一半. 我們可以給到使用者這個計數值, 另外,我們可以根據該值得到後續分頁還有多少資料, 如果沒有自然不用再查了, 如果有則組裝limit語句.

    // com.github.pagehelper.dialect.AbstractHelperDialect#afterCount
    @Override
    public boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {
        Page page = getLocalPage();
        page.setTotal(count);
        if (rowBounds instanceof PageRowBounds) {
            ((PageRowBounds) rowBounds).setTotal(count);
        }
        //pageSize < 0 的時候,不執行分頁查詢
        //pageSize = 0 的時候,還需要執行後續查詢,但是不會分頁
        if (page.getPageSize() < 0) {
            return false;
        }
        // 還沒到最後一頁, 則需要進行分頁查詢
        return count > ((page.getPageNum() - 1) * page.getPageSize());
    }
    
    /**
     * 分頁查詢
     */
    public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                 RowBounds rowBounds, ResultHandler resultHandler,
                                 BoundSql boundSql, CacheKey cacheKey) throws SQLException {
        //判斷是否需要進行分頁查詢
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //生成分頁的快取 key
            CacheKey pageKey = cacheKey;
            //處理引數物件, 將會加入 pageStart, pageSize 等引數
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //呼叫方言獲取分頁 sql
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //設定動態引數
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //執行分頁查詢
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不執行分頁的情況下,也不執行記憶體分頁
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
    }
    // com.github.pagehelper.dialect.AbstractHelperDialect#processParameterObject
    @Override
    public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
        //處理引數
        Page page = getLocalPage();
        //如果只是 order by 就不必處理引數
        if (page.isOrderByOnly()) {
            return parameterObject;
        }
        Map<String, Object> paramMap = null;
        if (parameterObject == null) {
            paramMap = new HashMap<String, Object>();
        } else if (parameterObject instanceof Map) {
            //解決不可變Map的情況
            paramMap = new HashMap<String, Object>();
            paramMap.putAll((Map) parameterObject);
        } else {
            paramMap = new HashMap<String, Object>();
            //動態sql時的判斷條件不會出現在ParameterMapping中,但是必須有,所以這裡需要收集所有的getter屬性
            //TypeHandlerRegistry可以直接處理的會作為一個直接使用的物件進行處理
            boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
            MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
            //需要針對註解形式的MyProviderSqlSource儲存原值
            if (!hasTypeHandler) {
                for (String name : metaObject.getGetterNames()) {
                    paramMap.put(name, metaObject.getValue(name));
                }
            }
            //下面這段方法,主要解決一個常見型別的引數時的問題
            if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {
                for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
                    String name = parameterMapping.getProperty();
                    if (!name.equals(PAGEPARAMETER_FIRST)
                            && !name.equals(PAGEPARAMETER_SECOND)
                            && paramMap.get(name) == null) {
                        if (hasTypeHandler
                                || parameterMapping.getJavaType().equals(parameterObject.getClass())) {
                            paramMap.put(name, parameterObject);
                            break;
                        }
                    }
                }
            }
        }
        return processPageParameter(ms, paramMap, page, boundSql, pageKey);
    }

    // 加入 page 引數
    // com.github.pagehelper.dialect.helper.MySqlDialect#processPageParameter
    @Override
    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        // First_PageHelper, Second_PageHelper
        paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
        paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
        //處理pageKey
        pageKey.update(page.getStartRow());
        pageKey.update(page.getPageSize());
        //處理引數配置
        if (boundSql.getParameterMappings() != null) {
            List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
            if (page.getStartRow() == 0) {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build());
            } else {
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, Integer.class).build());
                newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, Integer.class).build());
            }
            MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
            metaObject.setValue("parameterMappings", newParameterMappings);
        }
        return paramMap;
    }
    // 組裝分頁sql
    // com.github.pagehelper.dialect.AbstractHelperDialect#getPageSql
    @Override
    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        Page page = getLocalPage();
        //支援 order by
        String orderBy = page.getOrderBy();
        if (StringUtil.isNotEmpty(orderBy)) {
            pageKey.update(orderBy);
            sql = OrderByParser.converToOrderBySql(sql, orderBy);
        }
        if (page.isOrderByOnly()) {
            return sql;
        }
        return getPageSql(sql, page, pageKey);
    }
    // com.github.pagehelper.dialect.helper.MySqlDialect#getPageSql
    @Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        // 分頁sql拼接, limit xxx
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ? ");
        } else {
            sqlBuilder.append(" LIMIT ?, ? ");
        }
        return sqlBuilder.toString();
    }
    

  經過上面的sql重組之後,就可以得到具體分頁的list資料了, 返回的也是list資料. 那麼, 使用者如何獲取其他的分頁資訊呢? 比如count值去了哪裡? 實際上, 在list 返回之後, 還有一個 afterPage 的動作要做, 而它的作用就是封裝list 為帶page資訊的list.

    // com.github.pagehelper.PageHelper#afterPage
    @Override
    public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
        //這個方法即使不分頁也會被執行,所以要判斷 null
        AbstractHelperDialect delegate = autoDialect.getDelegate();
        if (delegate != null) {
            return delegate.afterPage(pageList, parameterObject, rowBounds);
        }
        return pageList;
    }
    
    // com.github.pagehelper.dialect.AbstractHelperDialect#afterPage
    @Override
    public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
        // 取出本執行緒的page變數, 放入list
        Page page = getLocalPage();
        if (page == null) {
            return pageList;
        }
        page.addAll(pageList);
        // count 值臨時變換, 用於應對沒有進行count的場景, 使外部表現一致
        if (!page.isCount()) {
            page.setTotal(-1);
        } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
            page.setTotal(pageList.size());
        } else if(page.isOrderByOnly()){
            page.setTotal(pageList.size());
        }
        return page;
    }

  至此, 一個完整的分頁功能就完成了. 核心邏輯最開始也已看到, 就是判斷是否需要分頁, 是否需要count, 然後新增分頁sql取數的這麼個過程. 其本身並無太多銀彈, 但卻是能讓我們節省不少時間. 另外就是, 在應對資料庫可能發生切換的場景, 我們也可以無需更改此部分程式碼, 從而減輕了歷史負擔. 用用又何樂而不為呢?

  最後, 我們再來看下oracle的核心分頁的時候, 以理解pagehelper 的良苦用心.

5. oracle sql 變換

  前面我們以mysql為樣例, 看了pagehelper的轉換過程, 其核心自然是 對count和select sql 的變換. 下面我們看看oracle如何變換吧!

// com.github.pagehelper.dialect.helper.OracleDialect
public class OracleDialect extends AbstractHelperDialect {

    @Override
    public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
        paramMap.put(PAGEPARAMETER_FIRST, page.getEndRow());
        paramMap.put(PAGEPARAMETER_SECOND, page.getStartRow());
        //處理pageKey
        pageKey.update(page.getEndRow());
        pageKey.update(page.getStartRow());
        //處理引數配置
        handleParameter(boundSql, ms);
        return paramMap;
    }
    // 獲取帶分頁的sql
    @Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
        // 很明顯, oracle 和 mysql 的分頁實現是不一樣的, oracle 使用 row_id 實現, 而 mysql 使用 limit 實現 
        sqlBuilder.append("SELECT * FROM ( ");
        sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( ");
        sqlBuilder.append(sql);
        sqlBuilder.append(" ) TMP_PAGE)");
        sqlBuilder.append(" WHERE ROW_ID <= ? AND ROW_ID > ?");
        return sqlBuilder.toString();
    }

}

  從OracleDialect的實現中,我們看到它與mysql的差異僅在引數設定和獲取分頁sql時的差別, count 操作都是一樣的. 雖然是這樣, 但假設我們沒有使用分頁外掛, 那麼你會發現, 各個同學實現的count和分頁查詢相差甚大, 這必將給以後的改造帶來許多麻煩, 這就沒必要了.

  pagehelper 支援的幾個方言如下:

  它們與oracle的實現方式都差不多,也就是說 count 都一樣,只是分頁的sql不一樣而已。