1. 引入分頁插件
引入分頁插件有下面2種方式,推薦使用 Maven 方式。
原文地址:https://github.com/pagehelper/Mybatis-PageHelper/tree/master/src/main/java/com/github/pagehelper
1). 引入 Jar 包
你可以從下面的地址中下載最新版本的 jar 包
-
https://oss.sonatype.org/content/repositories/releases/com/github/pagehelper/pagehelper/
-
https://repo1.maven.org/maven2/com/github/pagehelper/pagehelper/
由於使用了sql 解析工具,你還需要下載 jsqlparser.jar(需要和PageHelper 依賴的版本一致) :
2). 使用 Maven
在 pom.xml 中添加如下依賴:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>最新版本</version> </dependency>
最新版本號可以從首頁查看。
2. 配置攔截器插件
特別注意,新版攔截器是 com.github.pagehelper.PageInterceptor
。 com.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 實現時,下面的參數沒有任何作用。
-
helperDialect
:分頁插件會自動檢測當前的數據庫鏈接,自動選擇合適的分頁方式。 你可以配置helperDialect
屬性來指定分頁插件使用哪種方言。配置時,可以使用下面的縮寫值:oracle
,mysql
,mariadb
,sqlite
,hsqldb
,postgresql
,db2
,sqlserver
,informix
,h2
,sqlserver2012
,derby
特別注意:使用 SqlServer2012 數據庫時,需要手動指定為sqlserver2012
,否則會使用 SqlServer2005 的方式進行分頁。
你也可以實現AbstractHelperDialect
,然后配置該屬性為實現類的全限定名稱即可使用自定義的實現方法。 -
offsetAsPageNum
:默認值為false
,該參數對使用RowBounds
作為分頁參數時有效。 當該參數設置為true
時,會將RowBounds
中的offset
參數當成pageNum
使用,可以用頁碼和頁面大小兩個參數進行分頁。 -
rowBoundsWithCount
:默認值為false
,該參數對使用RowBounds
作為分頁參數時有效。 當該參數設置為true
時,使用RowBounds
分頁會進行 count 查詢。 -
pageSizeZero
:默認值為false
,當該參數設置為true
時,如果pageSize=0
或者RowBounds.limit = 0
就會查詢出全部的結果(相當於沒有執行分頁查詢,但是返回結果仍然是Page
類型)。 -
reasonable
:分頁合理化參數,默認值為false
。當該參數設置為true
時,pageNum<=0
時會查詢第一頁,pageNum>pages
(超過總數時),會查詢最后一頁。默認false
時,直接根據參數進行查詢。 -
params
:為了支持startPage(Object params)
方法,增加了該參數來配置參數映射,用於從對象中根據屬性名取值, 可以配置pageNum,pageSize,count,pageSizeZero,reasonable
,不配置映射的用默認值, 默認值為pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
。 -
supportMethodsArguments
:支持通過 Mapper 接口參數來傳遞分頁參數,默認值false
,分頁插件會從查詢方法的參數值中,自動根據上面params
配置的字段中取值,查找到合適的值時就會自動分頁。 使用方法可以參考測試代碼中的com.github.pagehelper.test.basic
包下的ArgumentsMapTest
和ArgumentsObjTest
。 -
autoRuntimeDialect
:默認值為false
。設置為true
時,允許在運行時根據多數據源自動識別對應方言的分頁 (不支持自動選擇sqlserver2012
,只能使用sqlserver
),用法和注意事項參考下面的場景五。 -
closeConn
:默認值為true
。當使用運行時動態數據源或沒有設置helperDialect
屬性自動獲取數據庫類型時,會自動獲取一個數據庫連接, 通過該屬性來設置是否關閉獲取的這個連接,默認true
關閉,設置為false
后,不會關閉獲取的連接,這個參數的設置要根據自己選擇的數據源來決定。 -
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
使用,limit
和 pageSize
含義相同。
場景三
如果覺得某個地方使用分頁后,你仍然想通過控制參數查詢全部的結果,你可以配置 pageSizeZero
為 true
, 配置后,當 pageSize=0
或者 RowBounds.limit = 0
就會查詢出全部的結果。
場景四
如果你分頁插件使用於類似分頁查看列表式的數據,如新聞列表,軟件列表, 你希望用戶輸入的頁數不在合法范圍(第一頁到最后一頁之外)時能夠正確的響應到正確的結果頁面, 那么你可以配置 reasonable
為 true
,這時如果 pageNum<=0
會查詢第一頁,如果 pageNum>總頁數
會查詢最后一頁。
場景五
如果你在 Spring 中配置了動態數據源,並且連接不同類型的數據庫,這時你可以配置 autoRuntimeDialect
為 true
,這樣在使用不同數據源時,會使用匹配的分頁進行查詢。 這種情況下,你還需要特別注意 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
無法獲取查詢總數,分頁插件提供了一個繼承自 RowBounds
的 PageRowBounds
,這個對象中增加了 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);
當調用這個方法時,由於同時發現了 pageNumKey
和 pageSizeKey
參數,這個方法就會被分頁。params 提供的幾個參數都可以這樣使用。
除了上面這種方式外,如果 User 對象中包含這兩個參數值,也可以有下面的方法:
List<User> selectByPageNumSize(User user);
當從 User 中同時發現了 pageNumKey
和 pageSizeKey
參數,這個方法就會被分頁。
注意:pageNum
和 pageSize
兩個屬性同時存在才會觸發分頁操作,在這個前提下,其他的分頁參數才會生效。
3). PageHelper
安全調用
1. 使用 RowBounds
和 PageRowBounds
參數方式是極其安全的
2. 使用參數方式是極其安全的
3. 使用 ISelect 接口調用是極其安全的
ISelect 接口方式除了可以保證安全外,還特別實現了將查詢轉換為單純的 count 查詢方式,這個方法可以將任意的查詢方法,變成一個 select count(*)
的查詢方法。
4. 什么時候會導致不安全的分頁?
PageHelper
方法使用了靜態的 ThreadLocal
參數,分頁參數和線程是綁定的。
只要你可以保證在 PageHelper
方法調用后緊跟 MyBatis 查詢方法,這就是安全的。因為 PageHelper
在 finally
代碼段中自動清除了 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>(); }
這么寫很不好看,而且沒有必要。