分頁插件PageHelper、通用mapper
分頁插件github源代碼:https://github.com/pagehelper/Mybatis-PageHelper
通用mapper github源代碼:https://github.com/abel533/Mapper
1. 集成通用mapper
Java代碼集成、spring集成、springboot集成
Spring集成:
這是Mybatis最常用的一種環境,通用mapper提供了多種方式來和spring進行集成。
添加依賴:
在開始配置前,添加相關的依賴。
正常情況下,spring和mybaits的集成環境中,應該已經存在下面的依賴:
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>版本號</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>版本號</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>版本號</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>版本號</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>版本號</version> </dependency>
集成通用 Mapper 在上面的基礎上添加下面的依賴:
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>最新版本</version>
</dependency>
和 Spring 集成
和 Spring 進行集成時,分為 XML 和注解配置兩種方式,每種方式又有不同的配置方式。
這里提供了很多配置方式,使用時選擇一種改動最小的方式即可!
XML 配置
1.使用 MapperScannerConfigurer
和通用 Mapper 以前版本一樣,可以直接使用 tk.mybatis 提供的 tk.mybatis.spring.mapper.MapperScannerConfigurer 進行配置,這個配置和 MyBatis 官方提供的 org.mybatis.spring.mapper.MapperScannerConfigurer 區別只是第一層的包名,tk 和 org。所以使用這種方式時,如果你項目已經使用 org. 進行了配置,只需要改成 tk. 即可。
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="掃描包名"/>
</bean>
如果你需要對通用 Mapper 進行特殊配置,可以按下面的方式進行配置:
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="tk.mybatis.mapper.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <property name="properties"> <value> 參數名=值 參數名2=值2 ... </value> </property> </bean>
2.XML 配置使用 Configuration
如果某些第三方也需要特殊的 MapperScannerConfigurer 時,就不能用上面的方式進行配置了,此時可以選擇下面這種方式,這種方式要求使用MyBatis (3.4.0+) 和 mybatis-spring (1.3.0+),配置方式如下:
<!--使用 Configuration 方式進行配置--> <bean id="mybatisConfig" class="tk.mybatis.mapper.session.Configuration"> <!-- 配置通用 Mapper,有三種屬性注入方式 --> <property name="mapperProperties"> <value> notEmpty=true </value> </property> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configuration" ref="mybatisConfig"/> </bean> <!-- 不需要考慮下面這個,注意這里是 org 的 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="tk.mybatis.mapper.configuration"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean>
這里使用了 tk.mybatis.mapper.session.Configuration ,也就是不能通過讀取 mybatis-config.xml進行配置,上面這種配置更直接,使用 Spring setter 配置屬性更方便。當需要配置通用 Mapper 時,使用 mapperProperties 屬性配置即可,配置方式和前面的相同,一行一個配置即可。
配置了一個 mybatisConfig 的 bean 后,在 SqlSessionFactoryBean 中注入即可。
后面的 MapperScannerConfigurer 只是為了說明這里不需要使用 tk. 開頭的類進行配置。
這種配置方式基本上和任何第三方都不會沖突,如果你遇到了第三方重寫 SqlSessionFactoryBean 的情況,就使用前一種方式配置即可。
分頁插件如何使用?
引入分頁插件
1.使用 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<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10)); //第二種,Mapper接口方式的調用,推薦這種使用方式。PageHelper.startPage(1, 10);List<Country> list = countryMapper.selectIf(1); //第三種,Mapper接口方式的調用,推薦這種使用方式。PageHelper.offsetPage(1, 10);List<Country> list = countryMapper.selectIf(1); //第四種,參數方法調用//存在以下 Mapper 接口方法,你不需要在 xml 處理后兩個參數public interface CountryMapper { List<Country> selectByPageNumSize( @Param("user") User user, @Param("pageNum") int pageNum, @Param("pageSize") int pageSize); }//配置supportMethodsArguments=true//在代碼中直接調用:List<Country> list = countryMapper.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<Country> selectByPageNumSize(User user); }//當 user 中的 pageNum!= null && pageSize!= null 時,會自動分頁List<Country> list = countryMapper.selectByPageNumSize(user); //第六種,ISelect 接口方式//jdk6,7用法,創建接口Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() { @Override public void doSelect() { countryMapper.selectGroupBy(); } });//jdk8 lambda用法Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy()); //也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() { @Override public void doSelect() { countryMapper.selectGroupBy(); } });//對應的lambda用法 pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> countryMapper.selectGroupBy()); //count查詢,返回一個查詢語句的count數long total = PageHelper.count(new ISelect() { @Override public void doSelect() { countryMapper.selectLike(country); } });//lambda total = PageHelper.count(()->countryMapper.selectLike(country));
下面對最常用的方式進行詳細介紹
1). RowBounds方式的調用
List<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(1, 10));
使用這種調用方式時,你可以使用RowBounds參數進行分頁,這種方式侵入性最小,我們可以看到,通過RowBounds方式調用只是使用了這個參數,並沒有增加其他任何內容。
分頁插件檢測到使用了RowBounds參數時,就會對該查詢進行物理分頁。
關於這種方式的調用,有兩個特殊的參數是針對 RowBounds 的,你可以參看上面的 場景一 和 場景二
注:不只有命名空間方式可以用RowBounds,使用接口的時候也可以增加RowBounds參數,例如:
//這種情況下也會進行物理分頁查詢List<Country> selectAll(RowBounds rowBounds);
注意: 由於默認情況下的 RowBounds 無法獲取查詢總數,分頁插件提供了一個繼承自 RowBounds 的 PageRowBounds,這個對象中增加了 total 屬性,執行分頁查詢后,可以從該屬性得到查詢總數。
2). PageHelper.startPage 靜態方法調用
除了 PageHelper.startPage 方法外,還提供了類似用法的 PageHelper.offsetPage 方法。
在你需要進行分頁的 MyBatis 查詢方法前調用 PageHelper.startPage 靜態方法即可,緊跟在這個方法后的第一個MyBatis 查詢方法會被進行分頁。
例一:
//獲取第1頁,10條內容,默認查詢總數countPageHelper.startPage(1, 10);//緊跟着的第一個select方法會被分頁List<Country> list = countryMapper.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<Country> list = countryMapper.selectIf(1); //后面的不會被分頁,除非再次調用PageHelper.startPageList<Country> list2 = countryMapper.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條內容,默認查詢總數countPageHelper.startPage(1, 10);List<Country> list = countryMapper.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<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNumKey") int pageNum,
@Param("pageSizeKey") int pageSize);
當調用這個方法時,由於同時發現了 pageNumKey 和 pageSizeKey 參數,這個方法就會被分頁。params 提供的幾個參數都可以這樣使用。
除了上面這種方式外,如果 User 對象中包含這兩個參數值,也可以有下面的方法:
List<Country> 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<Country> list;if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
這種情況下由於 param1 存在 null 的情況,就會導致 PageHelper 生產了一個分頁參數,但是沒有被消費,這個參數就會一直保留在這個線程上。當這個線程再次被使用時,就可能導致不該分頁的方法去消費這個分頁參數,這就產生了莫名其妙的分頁。
上面這個代碼,應該寫成下面這個樣子:
List<Country> list;if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
這種寫法就能保證安全。
如果你對此不放心,你可以手動清理 ThreadLocal 存儲的分頁參數,可以像下面這樣使用:
List<Country> list;if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = countryMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList<Country>();
}
這么寫很不好看,而且沒有必要。
核心代碼簡單示例:
package cn.ltian.pageHelper.service.impl; import cn.ltian.pageHelper.dao.EmpDao; import cn.ltian.pageHelper.entity.Emp; import cn.ltian.pageHelper.service.EmpService; import cn.ltian.pageHelper.utils.PageResults; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Example; import java.util.List; @Service @Transactional(propagation=Propagation.REQUIRED,rollbackFor = Exception.class) public class EmpServiceImpl implements EmpService { @Autowired private EmpDao empDao; @Transactional(readOnly=true) public PageResults<Emp> queryUserListPaged(Emp emp, Integer page, Integer pageSize) { // 開始分頁 PageHelper.startPage(page, pageSize); //example Example example = new Example(Emp.class); Example.Criteria criteria = example.createCriteria(); //查詢 List<Emp> userList = empDao.selectByExample(example); //分頁工具類 PageResults<Emp> result = new PageResults(); //PageInfo PageInfo<Emp> p = new PageInfo<Emp>(userList); //result totalcount 查詢總數 result.setTotalCount((int)p.getTotal()); result.setResults(userList); return result; } }
Spring配置文件:
<!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!-- 自動掃描mapping.xml文件 --> <property name="mapperLocations" value="classpath:pageHelper/mapping/*.xml"></property> <!-- 配置分頁插件 --> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageHelper"> <property name="properties"> <value> dialect=mysql reasonable=true </value> </property> </bean> </array> </property> </bean> <!-- 配置tk.mybatis插件 --> <bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 此處value改成你的繼承tk.mapper的接口類包路徑 --> <property name="basePackage" value="cn.ltian.pageHelper.dao" /> </bean>
通用 Mapper 實現原理:https://blog.csdn.net/isea533/article/details/78493852
pageHelper源碼解析:https://my.oschina.net/zudajun/blog/745232
