1. 程式人生 > 實用技巧 >PageHelper分頁

PageHelper分頁

使用方法

1. 引入分頁外掛

引入分頁外掛有下面2種方式,推薦使用 Maven 方式。

1). 引入 Jar 包

你可以從下面的地址中下載最新版本的 jar 包

由於使用了sql 解析工具,你還需要下載 jsqlparser.jar(需要和PageHelper 依賴的版本一致) :

2). 使用 Maven

在 pom.xml 中新增如下依賴:

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

2. 配置攔截器外掛

特別注意,新版攔截器是com.github.pagehelper.PageInterceptorcom.github.pagehelper.PageHelper

現在是一個特殊的dialect實現類,是分頁外掛的預設實現類,提供了和以前相同的用法。

1. 在 MyBatis 配置 xml 中配置攔截器外掛

<!-- 
    plugins在配置檔案中的位置必須符合要求,否則會報錯,順序如下:
    properties?, settings?, 
    typeAliases?, typeHandlers?, 
    objectFactory?,objectWrapperFactory?, 
    plugins?, 
    environments?, databaseIdProvider?, mappers?
-->
<plugins>
    <!-- com.github.pagehelper為PageHelper類所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置引數,後面會有所有的引數介紹 -->
        <property name="param1" value="value1"/>
	</plugin>
</plugins>

2. 在 Spring 配置檔案中配置攔截器外掛

使用 spring 的屬性配置方式,可以使用plugins屬性像下面這樣配置:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <!-- 注意其他配置 -->
  <property name="plugins">
    <array>
      <bean class="com.github.pagehelper.PageInterceptor">
        <property name="properties">
          <!--使用下面的方式配置引數,一行配置一個 -->
          <value>
            params=value1
          </value>
        </property>
      </bean>
    </array>
  </property>
</bean>

3. 分頁外掛引數介紹

分頁外掛提供了多個可選引數,這些引數使用時,按照上面兩種配置方式中的示例配置即可。

分頁外掛可選引數如下:

  • dialect:預設情況下會使用 PageHelper 方式進行分頁,如果想要實現自己的分頁邏輯,可以實現Dialect(com.github.pagehelper.Dialect) 介面,然後配置該屬性為實現類的全限定名稱。

下面幾個引數都是針對預設 dialect 情況下的引數。使用自定義 dialect 實現時,下面的引數沒有任何作用。

  1. helperDialect:分頁外掛會自動檢測當前的資料庫連結,自動選擇合適的分頁方式。 你可以配置helperDialect屬性來指定分頁外掛使用哪種方言。配置時,可以使用下面的縮寫值:
    oracle,mysql,mariadb,sqlite,hsqldb,postgresql,db2,sqlserver,informix,h2,sqlserver2012,derby
    特別注意:使用 SqlServer2012 資料庫時,需要手動指定為sqlserver2012,否則會使用 SqlServer2005 的方式進行分頁。
    你也可以實現AbstractHelperDialect,然後配置該屬性為實現類的全限定名稱即可使用自定義的實現方法。

  2. offsetAsPageNum:預設值為false,該引數對使用RowBounds作為分頁引數時有效。 當該引數設定為true時,會將RowBounds中的offset引數當成pageNum使用,可以用頁碼和頁面大小兩個引數進行分頁。

  3. rowBoundsWithCount:預設值為false,該引數對使用RowBounds作為分頁引數時有效。 當該引數設定為true時,使用RowBounds分頁會進行 count 查詢。

  4. pageSizeZero:預設值為false,當該引數設定為true時,如果pageSize=0或者RowBounds.limit = 0就會查詢出全部的結果(相當於沒有執行分頁查詢,但是返回結果仍然是Page型別)。

  5. reasonable:分頁合理化引數,預設值為false。當該引數設定為true時,pageNum<=0時會查詢第一頁,pageNum>pages(超過總數時),會查詢最後一頁。預設false時,直接根據引數進行查詢。

  6. params:為了支援startPage(Object params)方法,增加了該引數來配置引數對映,用於從物件中根據屬性名取值, 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置對映的用預設值, 預設值為pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero

  7. supportMethodsArguments:支援通過 Mapper 介面引數來傳遞分頁引數,預設值false,分頁外掛會從查詢方法的引數值中,自動根據上面params配置的欄位中取值,查詢到合適的值時就會自動分頁。 使用方法可以參考測試程式碼中的com.github.pagehelper.test.basic包下的ArgumentsMapTestArgumentsObjTest

  8. autoRuntimeDialect:預設值為false。設定為true時,允許在執行時根據多資料來源自動識別對應方言的分頁 (不支援自動選擇sqlserver2012,只能使用sqlserver),用法和注意事項參考下面的場景五。

  9. closeConn:預設值為true。當使用執行時動態資料來源或沒有設定helperDialect屬性自動獲取資料庫型別時,會自動獲取一個數據庫連線, 通過該屬性來設定是否關閉獲取的這個連線,預設true關閉,設定為false後,不會關閉獲取的連線,這個引數的設定要根據自己選擇的資料來源來決定。

  10. aggregateFunctions(5.1.5+):預設為所有常見資料庫的聚合函式,允許手動新增聚合函式(影響行數),所有以聚合函式開頭的函式,在進行 count 轉換時,會套一層。其他函式和列會被替換為 count(0),其中count列可以自己配置。

重要提示:

offsetAsPageNum=false的時候,由於PageNum問題,RowBounds查詢的時候reasonable會強制為false。使用PageHelper.startPage方法不受影響。

4. 如何選擇配置這些引數

單獨看每個引數的說明可能是一件讓人不爽的事情,這裡列舉一些可能會用到某些引數的情況。

場景一

如果你仍然在用類似ibatis式的名稱空間呼叫方式,你也許會用到rowBoundsWithCount, 分頁外掛對RowBounds支援和 MyBatis 預設的方式是一致,預設情況下不會進行 count 查詢,如果你想在分頁查詢時進行 count 查詢, 以及使用更強大的PageInfo類,你需要設定該引數為true

注:PageRowBounds想要查詢總數也需要配置該屬性為true

場景二

如果你仍然在用類似ibatis式的名稱空間呼叫方式,你覺得RowBounds中的兩個引數offset,limit不如pageNum,pageSize容易理解, 你可以使用offsetAsPageNum引數,將該引數設定為true後,offset會當成pageNum使用,limitpageSize含義相同。

場景三

如果覺得某個地方使用分頁後,你仍然想通過控制引數查詢全部的結果,你可以配置pageSizeZerotrue, 配置後,當pageSize=0或者RowBounds.limit = 0就會查詢出全部的結果。

場景四

如果你分頁外掛使用於類似分頁檢視列表式的資料,如新聞列表,軟體列表, 你希望使用者輸入的頁數不在合法範圍(第一頁到最後一頁之外)時能夠正確的響應到正確的結果頁面, 那麼你可以配置reasonabletrue,這時如果pageNum<=0會查詢第一頁,如果pageNum>總頁數會查詢最後一頁。

場景五

如果你在 Spring 中配置了動態資料來源,並且連線不同型別的資料庫,這時你可以配置autoRuntimeDialecttrue,這樣在使用不同資料來源時,會使用匹配的分頁進行查詢。 這種情況下,你還需要特別注意closeConn引數,由於獲取資料來源型別會獲取一個數據庫連線,所以需要通過這個引數來控制獲取連線後,是否關閉該連線。 預設為true,有些資料庫連線關閉後就沒法進行後續的資料庫操作。而有些資料庫連線不關閉就會很快由於連線數用完而導致資料庫無響應。所以在使用該功能時,特別需要注意你使用的資料來源是否需要關閉資料庫連線。

當不使用動態資料來源而只是自動獲取helperDialect時,資料庫連線只會獲取一次,所以不需要擔心佔用的這一個連線是否會導致資料庫出錯,但是最好也根據資料來源的特性選擇是否關閉連線。

3. 如何在程式碼中使用

閱讀前請注意看重要提示

分頁外掛支援以下幾種呼叫方式:

//第一種,RowBounds方式的呼叫
List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));

//第二種,Mapper介面方式的呼叫,推薦這種使用方式。
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectIf(1);

//第三種,Mapper介面方式的呼叫,推薦這種使用方式。
PageHelper.offsetPage(1, 10);
List<User> list = userMapper.selectIf(1);

//第四種,引數方法呼叫
//存在以下 Mapper 介面方法,你不需要在 xml 處理後兩個引數
public interface CountryMapper {
    List<User> selectByPageNumSize(
            @Param("user") User user,
            @Param("pageNum") int pageNum, 
            @Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在程式碼中直接呼叫:
List<User> list = userMapper.selectByPageNumSize(user, 1, 10);

//第五種,引數物件
//如果 pageNum 和 pageSize 存在於 User 物件中,只要引數有值,也會被分頁
//有如下 User 物件
public class User {
    //其他fields
    //下面兩個引數名和 params 配置的名字一致
    private Integer pageNum;
    private Integer pageSize;
}
//存在以下 Mapper 介面方法,你不需要在 xml 處理後兩個引數
public interface CountryMapper {
    List<User> selectByPageNumSize(User user);
}
//當 user 中的 pageNum!= null && pageSize!= null 時,會自動分頁
List<User> list = userMapper.selectByPageNumSize(user);

//第六種,ISelect 介面方式
//jdk6,7用法,建立介面
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
    @Override
    public void doSelect() {
        userMapper.selectGroupBy();
    }
});
//jdk8 lambda用法
Page<User> page = PageHelper.startPage(1, 10).doSelectPage(()-> userMapper.selectGroupBy());

//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
    @Override
    public void doSelect() {
        userMapper.selectGroupBy();
    }
});
//對應的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> userMapper.selectGroupBy());

//count查詢,返回一個查詢語句的count數
long total = PageHelper.count(new ISelect() {
    @Override
    public void doSelect() {
        userMapper.selectLike(user);
    }
});
//lambda
total = PageHelper.count(()->userMapper.selectLike(user));

下面對最常用的方式進行詳細介紹

1). RowBounds方式的呼叫

List<User> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(1, 10));

使用這種呼叫方式時,你可以使用RowBounds引數進行分頁,這種方式侵入性最小,我們可以看到,通過RowBounds方式呼叫只是使用了這個引數,並沒有增加其他任何內容。

分頁外掛檢測到使用了RowBounds引數時,就會對該查詢進行物理分頁。

關於這種方式的呼叫,有兩個特殊的引數是針對RowBounds的,你可以參看上面的場景一和場景二

注:不只有名稱空間方式可以用RowBounds,使用介面的時候也可以增加RowBounds引數,例如:

//這種情況下也會進行物理分頁查詢
List<User> selectAll(RowBounds rowBounds);

注意:由於預設情況下的RowBounds無法獲取查詢總數,分頁外掛提供了一個繼承自RowBoundsPageRowBounds,這個物件中增加了total屬性,執行分頁查詢後,可以從該屬性得到查詢總數。

2).PageHelper.startPage靜態方法呼叫

除了PageHelper.startPage方法外,還提供了類似用法的PageHelper.offsetPage方法。

在你需要進行分頁的 MyBatis 查詢方法前呼叫PageHelper.startPage靜態方法即可,緊跟在這個方法後的第一個MyBatis 查詢方法會被進行分頁。

例一:
//獲取第1頁,10條內容,預設查詢總數count
PageHelper.startPage(1, 10);
//緊跟著的第一個select方法會被分頁
List<User> list = userMapper.selectIf(1);
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分頁時,實際返回的結果list型別是Page<E>,如果想取出分頁資訊,需要強制轉換為Page<E>
assertEquals(182, ((Page) list).getTotal());
例二:
//request: url?pageNum=1&pageSize=10
//支援 ServletRequest,Map,POJO 物件,需要配合 params 引數
PageHelper.startPage(request);
//緊跟著的第一個select方法會被分頁
List<User> list = userMapper.selectIf(1);

//後面的不會被分頁,除非再次呼叫PageHelper.startPage
List<User> list2 = userMapper.selectIf(null);
//list1
assertEquals(2, list.get(0).getId());
assertEquals(10, list.size());
//分頁時,實際返回的結果list型別是Page<E>,如果想取出分頁資訊,需要強制轉換為Page<E>,
//或者使用PageInfo類(下面的例子有介紹)
assertEquals(182, ((Page) list).getTotal());
//list2
assertEquals(1, list2.get(0).getId());
assertEquals(182, list2.size());
例三,使用PageInfo的用法:
//獲取第1頁,10條內容,預設查詢總數count
PageHelper.startPage(1, 10);
List<User> list = userMapper.selectAll();
//用PageInfo對結果進行包裝
PageInfo page = new PageInfo(list);
//測試PageInfo全部屬性
//PageInfo包含了非常全面的分頁屬性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
assertEquals(true, page.isHasNextPage());

3). 使用引數方式

想要使用引數方式,需要配置supportMethodsArguments引數為true,同時要配置params引數。 例如下面的配置:

<plugins>
    <!-- com.github.pagehelper為PageHelper類所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置引數,後面會有所有的引數介紹 -->
        <property name="supportMethodsArguments" value="true"/>
        <property name="params" value="pageNum=pageNumKey;pageSize=pageSizeKey;"/>
	</plugin>
</plugins>

在 MyBatis 方法中:

List<User> selectByPageNumSize(
        @Param("user") User user,
        @Param("pageNumKey") int pageNum, 
        @Param("pageSizeKey") int pageSize);

當呼叫這個方法時,由於同時發現了pageNumKeypageSizeKey引數,這個方法就會被分頁。params 提供的幾個引數都可以這樣使用。

除了上面這種方式外,如果 User 物件中包含這兩個引數值,也可以有下面的方法:

List<User> selectByPageNumSize(User user);

當從 User 中同時發現了pageNumKeypageSizeKey引數,這個方法就會被分頁。

注意:pageNumpageSize兩個屬性同時存在才會觸發分頁操作,在這個前提下,其他的分頁引數才會生效。

3).PageHelper安全呼叫

1. 使用RowBoundsPageRowBounds引數方式是極其安全的
2. 使用引數方式是極其安全的
3. 使用 ISelect 介面呼叫是極其安全的

ISelect 介面方式除了可以保證安全外,還特別實現了將查詢轉換為單純的 count 查詢方式,這個方法可以將任意的查詢方法,變成一個select count(*)的查詢方法。

4. 什麼時候會導致不安全的分頁?

PageHelper方法使用了靜態的ThreadLocal引數,分頁引數和執行緒是繫結的。

只要你可以保證在PageHelper方法呼叫後緊跟 MyBatis 查詢方法,這就是安全的。因為PageHelperfinally程式碼段中自動清除了ThreadLocal儲存的物件。

如果程式碼在進入Executor前發生異常,就會導致執行緒不可用,這屬於人為的 Bug(例如介面方法和 XML 中的不匹配,導致找不到MappedStatement時), 這種情況由於執行緒不可用,也不會導致ThreadLocal引數被錯誤的使用。

但是如果你寫出下面這樣的程式碼,就是不安全的用法:

PageHelper.startPage(1, 10);
List<User> list;
if(param1 != null){
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<User>();
}

這種情況下由於 param1 存在 null 的情況,就會導致 PageHelper 生產了一個分頁引數,但是沒有被消費,這個引數就會一直保留在這個執行緒上。當這個執行緒再次被使用時,就可能導致不該分頁的方法去消費這個分頁引數,這就產生了莫名其妙的分頁。

上面這個程式碼,應該寫成下面這個樣子:

List<User> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    list = userMapper.selectIf(param1);
} else {
    list = new ArrayList<User>();
}

這種寫法就能保證安全。

如果你對此不放心,你可以手動清理ThreadLocal儲存的分頁引數,可以像下面這樣使用:

List<User> list;
if(param1 != null){
    PageHelper.startPage(1, 10);
    try{
        list = userMapper.selectAll();
    } finally {
        PageHelper.clearPage();
    }
} else {
    list = new ArrayList<User>();
}

這麼寫很不好看,而且沒有必要。

4. MyBatis 和 Spring 整合示例

如果和Spring整合不熟悉,可以參考下面兩個

只有基礎的配置資訊,沒有任何現成的功能,作為新手入門搭建框架的基礎

這兩個整合框架集成了 PageHelper 和通用 Mapper

5. Spring Boot 整合示例