1. 程式人生 > >Mybatis分頁外掛PageHelper的配置和使用方法

Mybatis分頁外掛PageHelper的配置和使用方法

  • 前言

在web開發過程中涉及到表格時,例如dataTable,就會產生分頁的需求,通常我們將分頁方式分為兩種:前端分頁和後端分頁。

前端分頁

一次性請求資料表格中的所有記錄(ajax),然後在前端快取並且計算count和分頁邏輯,一般前端元件(例如dataTable)會提供分頁動作。

特點是:簡單,很適合小規模的web平臺;當資料量大的時候會產生效能問題,在查詢和網路傳輸的時間會很長。

後端分頁

在ajax請求中指定頁碼(pageNum)和每頁的大小(pageSize),後端查詢出當頁的資料返回,前端只負責渲染。

特點是:複雜一些;效能瓶頸在MySQL的查詢效能,這個當然可以調優解決。一般來說,web開發使用的是這種方式。

我們說的也是後端分頁。

  • MySQL對分頁的支援

簡單來說MySQL對分頁的支援是通過limit子句。請看下面的例子。

複製程式碼

limit關鍵字的用法是
LIMIT [offset,] rows
offset是相對於首行的偏移量(首行是0),rows是返回條數。

# 每頁10條記錄,取第一頁,返回的是前10條記錄
select * from tableA limit 0,10;
# 每頁10條記錄,取第二頁,返回的是第11條記錄,到第20條記錄,
select * from tableA limit 10,10;

複製程式碼

這裡提一嘴的是,MySQL在處理分頁的時候是這樣的:

limit 1000,10 - 過濾出1010條資料,然後丟棄前1000條,保留10條。當偏移量大的時候,效能會有所下降。

limit 100000,10 - 會過濾10w+10條資料,然後丟棄前10w條。如果在分頁中發現了效能問題,可以根據這個思路調優。

  • Mybatis分頁外掛PageHelper

在使用Java Spring開發的時候,Mybatis算是對資料庫操作的利器了。不過在處理分頁的時候,Mybatis並沒有什麼特別的方法,一般需要自己去寫limit子句實現,成本較高。好在有個PageHelper外掛。

1、POM依賴

Mybatis的配置就不多提了。PageHelper的依賴如下。需要新的版本可以去maven上自行選擇

1

2

3

4

5

<dependency>

<groupId>com.github.pagehelper</groupId>

<artifactId>pagehelper</artifactId>

<version>4.1.4</version>

</dependency>

2、Mybatis對PageHelper的配置

開啟Mybatis配置檔案,一般在Resource路徑下。我這裡叫mybatis-config.xml。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

<?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>

<!-- 全域性引數 -->

<settings>

<!-- 使全域性的對映器啟用或禁用快取。 -->

<setting name="cacheEnabled" value="true"/>

<!-- 全域性啟用或禁用延遲載入。當禁用時,所有關聯物件都會即時載入。 -->

<setting name="lazyLoadingEnabled" value="true"/>

<!-- 當啟用時,有延遲載入屬性的物件在被呼叫時將會完全載入任意屬性。否則,每種屬性將會按需要載入。 -->

<setting name="aggressiveLazyLoading" value="true"/>

<!-- 是否允許單條sql 返回多個數據集  (取決於驅動的相容性) default:true -->

<setting name="multipleResultSetsEnabled" value="true"/>

<!-- 是否可以使用列的別名 (取決於驅動的相容性) default:true -->

<setting name="useColumnLabel" value="true"/>

<!-- 允許JDBC 生成主鍵。需要驅動器支援。如果設為了true,這個設定將強制使用被生成的主鍵,有一些驅動器不相容不過仍然可以執行。  default:false  -->

<setting name="useGeneratedKeys" value="true"/>

<!-- 指定 MyBatis 如何自動對映 資料基表的列 NONE:不隱射 PARTIAL:部分  FULL:全部  -->

<setting name="autoMappingBehavior" value="PARTIAL"/>

<!-- 這是預設的執行型別  (SIMPLE: 簡單; REUSE: 執行器可能重複使用prepared statements語句;BATCH: 執行器可以重複執行語句和批量更新)  -->

<setting name="defaultExecutorType" value="SIMPLE"/>

<!-- 使用駝峰命名法轉換欄位。 -->

<setting name="mapUnderscoreToCamelCase" value="true"/>

<!-- 設定本地快取範圍 session:就會有資料的共享  statement:語句範圍 (這樣就不會有資料的共享 ) defalut:session -->

<setting name="localCacheScope" value="SESSION"/>

<!-- 設定但JDBC型別為空時,某些驅動程式 要指定值,default:OTHER,插入空值時不需要指定型別 -->

<setting name="jdbcTypeForNull" value="NULL"/>

</settings>

<plugins>

<plugin interceptor="com.github.pagehelper.PageHelper">

<property name="dialect" value="mysql"/>

<property name="offsetAsPageNum" value="false"/>

<property name="rowBoundsWithCount" value="false"/>

<property name="pageSizeZero" value="true"/>

<property name="reasonable" value="false"/>

<property name="supportMethodsArguments" value="false"/>

<property name="returnPageInfo" value="none"/>

</plugin>

</plugins>

</configuration>  

這裡要注意的是PageHelper相關的配置。 

如果你沒有載入Mybatis配置檔案,那麼使用的是Mybatis預設的配置。如何載入Mybatis配置檔案呢?

到你的dataSrouce配置中。

在配置sqlSessionFactory的時候,指定Mybatis核心配置檔案和mapper的路徑,程式碼如下

1

2

3

4

5

6

7

8

9

@Bean(name = "moonlightSqlSessionFactory")

@Primary

public SqlSessionFactory moonlightSqlSessionFactory(@Qualifier("moonlightData") DataSource dataSource) throws Exception {

SqlSessionFactoryBean bean = new SqlSessionFactoryBean();

bean.setDataSource(dataSource);

bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis-mapper/*.xml"));

bean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));

return bean.getObject();

} 

說明:

這裡配置的mapper.xml存放路徑,在Resource/mybatis-mapper資料夾下

這裡配置的mybatis-config.xml檔案,在Resource/下

3、分頁

準備一個mapper.xml,測試就隨便寫一個吧,乾脆就用工程裡的一個。

這裡這個查詢,是一個典型的多條件查詢,我們要做的是對多條件匹配到的記錄進行分頁。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.kangaroo.studio.moonlight.dao.mapper.MoonlightMapper">

<resultMap id="geoFenceList" type="com.kangaroo.studio.moonlight.dao.model.GeoFence">

<constructor>

<idArg column="id" javaType="java.lang.Integer" jdbcType="INTEGER" />

<arg column="name" javaType="java.lang.String" jdbcType="VARCHAR" />

<arg column="type" javaType="java.lang.Integer" jdbcType="INTEGER" />

<arg column="group" javaType="java.lang.String" jdbcType="VARCHAR" />

<arg column="geo" javaType="java.lang.String" jdbcType="VARCHAR" />

<arg column="createTime" javaType="java.lang.String" jdbcType="VARCHAR" />

<arg column="updateTime" javaType="java.lang.String" jdbcType="VARCHAR" />

</constructor>

</resultMap>

<sql id="base_column">id, name, type, `group`, geo, createTime, updateTime </sql>

<select id="queryGeoFence" parameterType="com.kangaroo.studio.moonlight.dao.model.GeoFenceQueryParam" resultMap="geoFenceList">

select <include refid="base_column"/> from geoFence where 1=1

<if test="type != null">

and type = #{type}

</if>

<if test="name != null">

and name like concat('%', #{name},'%')

</if>

<if test="group != null">

and `group` like concat('%', #{group},'%')

</if>

<if test="startTime != null">

and createTime >= #{startTime}

</if>

<if test="endTime != null">

and createTime <= #{endTime}

</if>

</select>

</mapper>

在Mapper.java介面中編寫對應的方法

1

List<GeoFence> queryGeoFence(GeoFenceQueryParam geoFenceQueryParam);

先上分頁程式碼,後面再說明

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

@RequestMapping(value = "/fence/query", method = RequestMethod.POST)

@ResponseBody

public ResponseEntity<Response> queryFence(@RequestBody GeoFenceQueryParam geoFenceQueryParam) {

try {

Map<String, Object> data = new HashMap<>();

Integer pageNum = geoFenceQueryParam.getPageNum()!=null?geoFenceQueryParam.getPageNum():1;

Integer pageSize = geoFenceQueryParam.getPageSize()!=null?geoFenceQueryParam.getPageSize():10;

Page page = PageHelper.startPage(pageNum, pageSize, true);

List<GeoFence> list = moonlightMapper.queryGeoFence(geoFenceQueryParam);

data.put("total", page.getTotal());

data.put("nowPage", pageNum);

data.put("data", list);

return new ResponseEntity<>(

new Response(ResultCode.SUCCESS, "查詢geoFence成功", data),

HttpStatus.OK);

} catch (Exception e) {

logger.error("查詢geoFence失敗", e);

return new ResponseEntity<>(

new Response(ResultCode.EXCEPTION, "查詢geoFence失敗", null),

HttpStatus.INTERNAL_SERVER_ERROR);

}

}

說明:

1、PageHelper的優點是,分頁和Mapper.xml完全解耦。實現方式是以外掛的形式,對Mybatis執行的流程進行了強化,添加了總數count和limit查詢。屬於物理分頁。

2、Page page = PageHelper.startPage(pageNum, pageSize, true); - true表示需要統計總數,這樣會多進行一次請求select count(0); 省略掉true引數只返回分頁資料。 

1)統計總數,(將SQL語句變為 select count(0) from xxx,只對簡單SQL語句其效果,複雜SQL語句需要自己寫)

    Page<?> page = PageHelper.startPage(1,-1);

    long count = page.getTotal();

2)分頁,pageNum - 第N頁, pageSize - 每頁M條數

    A、只分頁不統計(每次只執行分頁語句)

    PageHelper.startPage([pageNum],[pageSize]);

    List<?> pagelist = queryForList( xxx.class, "queryAll" , param);

    //pagelist就是分頁之後的結果

    B、分頁並統計(每次執行2條語句,一條select count語句,一條分頁語句)適用於查詢分頁時資料發生變動,需要將實時的變動資訊反映到分頁結果上

    Page<?> page = PageHelper.startPage([pageNum],[pageSize],[iscount]);

    List<?> pagelist = queryForList( xxx.class , "queryAll" , param);

    long count = page.getTotal();

    //也可以 List<?> pagelist = page.getList();  獲取分頁後的結果集

3)使用PageHelper查全部(不分頁)

    PageHelper.startPage(1,0);

    List<?> alllist = queryForList( xxx.class , "queryAll" , param);

4)PageHelper的其他API

    String orderBy = PageHelper.getOrderBy();    //獲取orderBy語句

    Page<?> page = PageHelper.startPage(Object params);

    Page<?> page = PageHelper.startPage(int pageNum, int pageSize);

    Page<?> page = PageHelper.startPage(int pageNum, int pageSize, boolean isCount);

    Page<?> page = PageHelper.startPage(pageNum, pageSize, orderBy);

    Page<?> page = PageHelper.startPage(pageNum, pageSize, isCount, isReasonable);    //isReasonable分頁合理化,null時用預設配置

    Page<?> page = PageHelper.startPage(pageNum, pageSize, isCount, isReasonable, isPageSizeZero);    //isPageSizeZero是否支援PageSize為0,true且pageSize=0時返回全部結果,false時分頁,null時用預設配置

5)、預設值

    //RowBounds引數offset作為PageNum使用 - 預設不使用

    private boolean offsetAsPageNum = false;

    //RowBounds是否進行count查詢 - 預設不查詢

    private boolean rowBoundsWithCount = false;

    //當設定為true的時候,如果pagesize設定為0(或RowBounds的limit=0),就不執行分頁,返回全部結果

    private boolean pageSizeZero = false;

    //分頁合理化

    private boolean reasonable = false;

    //是否支援介面引數來傳遞分頁引數,預設false

    private boolean supportMethodsArguments = false;  

3、有一個安全性問題,需要注意一下,不然可能導致分頁錯亂。我這裡直接貼上了這篇部落格裡的一段話。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

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>();

}

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