Mybatis分頁那點事
前言
分頁可以說是非常常見的一個功能,大部分主流的資料庫都提供了物理分頁方式,比如Mysql的limit關鍵字,Oracle的ROWNUM關鍵字等;Mybatis作為一個ORM框架,也提供了分頁功能,接下來詳細介紹Mybatis的分頁功能。
RowBounds分頁
1.RowBounds介紹
Mybatis提供了RowBounds類進行分頁處理,內部提供了offset和limit兩個值,分別用來指定查詢資料的開始位置和查詢資料量:
public class RowBounds {
public static final int NO_ROW_OFFSET = 0;
public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
public static final RowBounds DEFAULT = new RowBounds();
private final int offset;
private final int limit ;
public RowBounds() {
this.offset = NO_ROW_OFFSET;
this.limit = NO_ROW_LIMIT;
}
}
複製程式碼
預設是從0下標開始,查詢資料量為Integer.MAX_VALUE;查詢的時候沒有指定RowBounds的時候預設RowBounds.DEFAULT:
public <E> List<E> selectList(String statement,Object parameter) {
return this.selectList(statement,parameter,RowBounds.DEFAULT);
}
複製程式碼
2.RowBounds使用
使用也很簡單,只需要知道總記錄數,然後設定好每頁需要查詢的數量,計算出一共分多少次查詢,然後通過RowBounds指定下標,大致程式碼如下:
public String rowBounds() {
int pageSize = 10;
int totalCount = blogRepository.countBlogs();
int totalPages = (totalCount % pageSize == 0) ? totalCount / pageSize : totalCount / pageSize + 1;
System.out.println("[pageSize=" + pageSize + ",totalCount=" + totalCount + ",totalPages=" + totalPages + "]");
for (int currentPage = 0; currentPage < totalPages; currentPage++) {
List<Blog> blogs = blogRepository.selectBlogs("zhaohui",new RowBounds(currentPage * pageSize,pageSize));
System.err.println("currentPage=" + (currentPage + 1) + ",current size:" + blogs.size());
}
return "ok";
}
複製程式碼
pageSize每次查詢數量,totalCount總記錄數,totalPages總共分多少次查詢;
3.RowBounds分析
Mybatis處理分頁的相關程式碼在DefaultResultSetHandler中,部分程式碼如下:
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw,ResultMap resultMap,ResultHandler<?> resultHandler,RowBounds rowBounds,ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
ResultSet resultSet = rsw.getResultSet();
//跳過指定的下標Offset
skipRows(resultSet,rowBounds);
////判定當前是否讀取是否在limit內
while (shouldProcessMoreRows(resultContext,rowBounds) && !resultSet.isClosed() && resultSet.next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet,resultMap,null);
Object rowValue = getRowValue(rsw,discriminatedResultMap,null);
storeObject(resultHandler,resultContext,rowValue,parentMapping,resultSet);
}
}
//跳過指定的下標Offset
private void skipRows(ResultSet rs,RowBounds rowBounds) throws SQLException {
if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
rs.absolute(rowBounds.getOffset());
}
} else {
for (int i = 0; i < rowBounds.getOffset(); i++) {
if (!rs.next()) {
break;
}
}
}
}
//判定當前是否讀取是否在limit內
private boolean shouldProcessMoreRows(ResultContext<?> context,RowBounds rowBounds) {
return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}
複製程式碼
在處理ResultSet首先需要跳過指定的下標Offset,這裡跳過方式分成了兩種情況:resultSetType為TYPE_FORWARD_ONLY和resultSetType為非TYPE_FORWARD_ONLY型別,Mybatis也提供了型別配置,可選項包括:
- FORWARD_ONLY:只能向前滾動;
- SCROLL_SENSITIVE: 能夠實現任意的前後滾動,對修改敏感;
- SCROLL_INSENSITIVE:能夠實現任意的前後滾動,對修不改敏感;
- DEFAULT:預設值為FORWARD_ONLY;
型別為FORWARD_ONLY的情況下只能遍歷到指定的下標,而其他兩種型別可以直接通過absolute方法定位到指定下標,可以通過如下方式指定型別:
<select id="selectBlogs" parameterType="string" resultType="blog" resultSetType="SCROLL_INSENSITIVE ">
select * from blog where author = #{author}
</select>
複製程式碼
limit限制,通過ResultContext中記錄的resultCount記錄當前讀取的記錄數,然後判定是否已經達到limit限制;
Pagehelper分頁
除了官方提供的RowBounds分頁方式,比較常用的有第三方外掛Pagehelper;為什麼已經有官方提供的分頁方式,還出現了Pagehelper這樣的第三方外掛,主要原因還是RowBounds提供的是邏輯分頁,而Pagehelper提供了物理分頁;
1.Pagehelper使用
Pagehelper主要利用了Mybatis的外掛功能,所以在使用的時候首先需要配置外掛類PageInterceptor:
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql" />
</plugin>
複製程式碼
helperDialect用來指定何種方言,這裡使用mysql進行測試;更多詳細的引數配置可以參考官方檔案:Mybatis-PageHelper;具體如何呼叫,Mybatis-PageHelper也提供了多種方式,這裡使用RowBounds方式的呼叫,具體程式碼和上面的例項程式碼完全一樣,只不過因為外掛的存在,使其實現方式發生改變;
2.Pagehelper分析
Pagehelper通過對Executor的query方法進行攔截,具體如下:
@Intercepts(
{
@Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}),@Signature(type = Executor.class,ResultHandler.class,CacheKey.class,BoundSql.class}),}
)
public class PageInterceptor implements Interceptor {
}
複製程式碼
在上文Mybatis之外掛分析中介紹了外掛利用了動態代理技術,在執行Executor的query方法時,會自動觸發InvocationHandler的invoke方法,方法內會呼叫PageInterceptor的intercept方法:
public Object invoke(Object proxy,Method method,Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target,method,args));
}
return method.invoke(target,args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
複製程式碼
可以看到最終query的相關引數args(4個或者6個),都封裝到了Invocation中,其中就包括了用於分頁的RowBounds類;Pagehelper會將RowBounds中的offset和limit對映到功能更強大的Page類, Page裡麵包含了很多屬性,這裡就簡單看一下和RowBounds相關的:
public Page(int[] rowBounds,boolean count) {
super(0);
if (rowBounds[0] == 0 && rowBounds[1] == Integer.MAX_VALUE) {
pageSizeZero = true;
this.pageSize = 0;
} else {
this.pageSize = rowBounds[1];
this.pageNum = rowBounds[1] != 0 ? (int) (Math.ceil(((double) rowBounds[0] + rowBounds[1]) / rowBounds[1])) : 0;
}
this.startRow = rowBounds[0];
this.count = count;
this.endRow = this.startRow + rowBounds[1];
}
複製程式碼
offset對映到了startRow,limit對映到了pageSize;有了相關分頁的引數,然後通過配置的資料庫方言型別,生成不同的方言生成sql,比如Mysql提供了MySqlRowBoundsDialect類:
public String getPageSql(String sql,CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (rowBounds.getOffset() == 0) {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(rowBounds.getLimit());
} else {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(rowBounds.getOffset());
sqlBuilder.append(",");
sqlBuilder.append(rowBounds.getLimit());
pageKey.update(rowBounds.getOffset());
}
pageKey.update(rowBounds.getLimit());
return sqlBuilder.toString();
}
複製程式碼
mysql的物理分頁關鍵字是Limit,提供offset和limit即可實現分頁;
效能對比
RowBounds利用的是邏輯分頁,而Pagehelper利用的物理分頁;
邏輯分頁:邏輯分頁利用遊標分頁,好處是所有資料庫都統一,壞處就是效率低;利用ResultSet的滾動分頁,由於ResultSet帶有遊標,因此可以使用其next()方法來指向下一條記錄;當然也可以利用Scrollable ResultSets(可滾動結果集合)來快速定位到某個遊標所指定的記錄行,所使用的是ResultSet的absolute()方法;
物理分頁:資料庫本身提供了分頁方式,如mysql的limit,好處是效率高,不好的地方就是不同資料庫有不同分頁方式,需要為每種資料庫單獨分頁處理;
下面分別對邏輯分頁向前滾動,邏輯分頁前後滾動,以及物理分頁三種分頁方式查詢100條資料進行測試,使用druid進行監控,使用的資料庫是mysql;
1.邏輯分頁向前滾動
因為只能向前滾動,所有越往後面的分頁,遍歷的資料越多,監控如下:
雖然只有100條資料,但是讀取資料為550行,效能低下;
2.邏輯分頁前後滾動
這裡配置的resultSetType為SCROLL_INSENSITIVE,可以快速定位,監控如下:
3.物理分頁
配置使用Pagehelper外掛,監控如下:
可以看到物理分頁在執行時間和讀取行數都更佔優;
總結
本文分別介紹了RowBounds和Pagehelper兩種分頁方式,分別代表了邏輯分頁和物理分頁;以及這兩種方式是內部是如何實現的;最後文末做了一個簡單的效能測試。