Mybatis的五種分頁方式詳解

 

第一種:LIMIT關鍵字

1,mapper代碼

select * from tb_user limit #{pageNo}, #{pageSize}

2,業務層直接調用

public List findByPageInfo(PageInfo info) {
    return userMapper.selectByPageInfo(info);
}

3,優點

靈活性高,可優化空間大
mysql分頁語句優化

4,缺點

實現複雜。

 

第二種:RowBounds實現分頁

Mybatis提供RowBounds類來實現邏輯分頁。RowBounds中有2個字段offset和limit。這種方式獲取所有的ResultSet,從ResultSet中的offset位置開始獲取limit個記錄。但這並不意味着JDBC驅動器會將所有的ResultSet存放在內存,實際上只加載小部分數據到內存,如果需要,再加載部分數據到內存。

1,mapper的xml代碼

select * from user

2, dao代碼

List<User> selectPage(RowBounds rowBounds);

3, 分頁查詢

   List<User> users = userMapper.selectPage(new RowBounds(5, 10));
   log.info("users:{}",users);

查詢結果:

users:[User(id=6, username=柳雲璇, grade=小三(5)班, age=25, phone=17358053274, sex=女), User(id=7, username=酆雨寒, grade=高一(5)班, age=19, phone=15394214112, sex=女), User(id=8, username=鄭春陽, grade=小三(7)班, age=24, phone=15004202411, sex=男)]

4, 優點

使用起來比直接limit簡單。

5,缺點

DB壓力比較大,因爲將結果暫存在db了。

 

第三種:藉助數組進行分頁(和上面的很相似,只是查詢出所有,自己通過subList()進行返回)

1,mapper接口代碼

List<Student> queryStudentsByArray();

2,mapper的xml代碼

 <select id="queryStudentsByArray"  resultMap="studentmapper">
        select * from student
 </select>

3,定義IStuService接口,並且定義分頁方法:

List<Student> queryStudentsByArray(int currPage, int pageSize);

通過接收currPage參數表示顯示第幾頁的數據,pageSize表示每頁顯示的數據條數。

創建IStuService接口實現類StuServiceIml對方法進行實現,對獲取到的數組通過currPage和pageSize進行分頁:

 @Override
    public List<Student> queryStudentsByArray(int currPage, int pageSize) {
        List<Student> students = studentMapper.queryStudentsByArray();
//        從第幾條數據開始
        int firstIndex = (currPage - 1) * pageSize;
//        到第幾條數據結束
        int lastIndex = currPage * pageSize;
        return students.subList(firstIndex, lastIndex);
    }

通過subList方法,獲取到兩個索引間的所有數據。

4,最後在controller中創建測試方法:

@ResponseBody
    @RequestMapping("/student/array/{currPage}/{pageSize}")
    public List<Student> getStudentByArray(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) {
        List<Student> student = StuServiceIml.queryStudentsByArray(currPage, pageSize);
        return student;
    }

通過用戶傳入的currPage和pageSize獲取指定數據。

他的有點和劣勢與上面第中方法類似。

 

第四種:Interceptor 實現

1,自定義Interceptor


@Intercepts({@Signature(type = StatementHandler.class, method = “prepare”, args = {Connection.class, Integer.class})})
public class DefinedPageInterceptor implements Interceptor {

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		//獲取StatementHandler,默認的是RoutingStatementHandler
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		//獲取StatementHandler的包裝類
		MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
		//分隔代理對象
		while (metaObject.hasGetter("h")) {
			Object obj = metaObject.getValue("h");
			metaObject = SystemMetaObject.forObject(obj);
		}
		while (metaObject.hasGetter("target")) {
			Object obj = metaObject.getValue("target");
			metaObject = SystemMetaObject.forObject(obj);
		}
		//獲取查看接口映射的相關信息
		MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
		String mapId = mappedStatement.getId();
		//攔截以ByInterceptor結尾的請求,統一實現分頁
		if (mapId.matches(".+ByInterceptor$")) {
			System.out.println("LOG:已觸發分頁攔截器");
			//獲取進行數據庫操作時管理參數的Handler
			ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
			// 分頁對象需要自己構建,到時候分頁方法中要傳入 
			//獲取請求時的參數
			PageInfo info = (PageInfo) parameterHandler.getParameterObject();
			//獲取原始SQL語句
			String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
			//構建分頁功能的SQL語句
			String sql = originalSql.trim() + " limit " + info.getPageNum() + ", " + info.getPageSize();
			metaObject.setValue("delegate.boundSql.sql", sql);
		}
		//調用原對象方法,進入責任鏈下一級
		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		//生成Object對象的動態代理對象
		return Plugin.wrap(target, this);
	}

	@Override
	public void setProperties(Properties properties) {
		//如果分頁每頁數量是統一的,可以在這裏進行統一配置,也就無需再傳入PageInfo信息了
	}


}

2,添加到mybatis中

@Bean(“oneSqlSessionFactory”)
public SqlSessionFactory sqlSessionFactory() {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSource);
    try {
        sqlSessionFactoryBean.setMapperLocations(
        // 設置mybatis的xml所在位置
        new PathMatchingResourcePatternResolver().getResources(“classpath*:mapping/one/*Mapper.xml”));
        //設置自定義的插件
        sqlSessionFactoryBean.setPlugins(new DefinedPageInterceptor());
        return sqlSessionFactoryBean.getObject();
    } catch (Exception e) {
        return null;
    }
}

3,mapper方法

/**
* Interceptor 實現分頁,必須以ByInterceptor結構,自定義的Interceptor才能
* 識別出來,並且必須傳入PageInfo
* @param pageInfo 自定義的分頁
* @return
*/
List selectPageByInterceptor(PageInfo pageInfo);

xml文件

    <select id="selectPageByInterceptor" resultType="com.example.demo.mapper.one.User">
        select * from user
    </select>

4, 運行

/**
* 自定義PageInterceptor分頁
*/
@Test
public void test04() {
    PageInfo pageInfo=new PageInfo();
    pageInfo.setPageNum(5);
    pageInfo.setPageSize(5);
    List users = userMapper.selectPageByInterceptor(pageInfo);
    log.info(“users:{}”,users);
}

攔截更改後的sql。

5, 運行結果

users:[User(id=6, username=柳雲璇, grade=小三(5)班, age=25, phone=17358053274, sex=女), User(id=7, username=酆雨寒, grade=高一(5)班, age=19, phone=15394214112, sex=女), User(id=8, username=鄭春陽, grade=小三(7)班, age=24, phone=15004202411, sex=男)]

 

第五種:PageHelper (他其實也是第4種的一個實現,用的多,就把他單獨描述爲一種)

1,官網

官網:https://pagehelper.github.io/

使用方式:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md

2, 添加pom依賴

com.github.pagehelper pagehelper 最新版本

<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
</dependency>

或者

<!-- 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.4.6</version>
</dependency>

特別注意,分頁和查詢直接不能有其他語句

 

要絕對保證PageHelper.startPage和分頁查詢語句之間不要有任何其他語句,或者在程序結束時增加PageHelper.clearPage();的調用

 

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

3). 在 Spring Boot 中配置

Spring Boot 引入 starter 後自動生效,對分頁插件進行配置時,在 Spring Boot 對應的配置文件 application.[properties|yaml] 中配置:

properties:

pagehelper.propertyName=propertyValue
pagehelper.reasonable=false
pagehelper.defaultCount=true

yaml:

pagehelper:
  propertyName: propertyValue
  reasonable: false
  defaultCount: true # 分頁插件默認參數支持 default-count 形式,自定義擴展的參數,必須大小寫一致

支持的默認參數參考: PageHelperStandardProperties.java

4). 分頁插件banner設置

爲了避免多次配置分頁插件導致的錯誤,配置分頁插件後,啓動時會輸出 banner。

DEBUG [main] -

,------.                           ,--.  ,--.         ,--.
|  .--. '  ,--,--.  ,---.   ,---.  |  '--'  |  ,---.  |  |  ,---.   ,---.  ,--.--.
|  '--' | ' ,-.  | | .-. | | .-. : |  .--.  | | .-. : |  | | .-. | | .-. : |  .--'
|  | --'  \ '-'  | ' '-' ' \   --. |  |  |  | \   --. |  | | '-' ' \   --. |  |
`--'       `--`--' .`-  /   `----' `--'  `--'  `----' `--' |  |-'   `----' `--'
`---'                                   `--'                        is intercepting.

如果在項目啓動時輸出了多次 banner,就是配置了多次分頁插件,根據日誌輸出的位置排查系統通過哪些方式配置了分頁插件。

如果不想在啓動時輸出 banner,可以通過系統變量或環境變量關閉。

  • 系統變量:-Dpagehelper.banner=false
  • 環境變量:PAGEHELPER_BANNER=false

如何在代碼中使用 

分頁插件支持以下幾種調用方式:

//第一種,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));

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

(一)、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 屬性,執行分頁查詢後,可以從該屬性得到查詢總數。

 

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

 

(三)、使用參數方式

想要使用參數方式,需要配置 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 兩個屬性同時存在纔會觸發分頁操作,在這個前提下,其他的分頁參數纔會生效。

 

(四)、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>();
}

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章