1. 程式人生 > 程式設計 >Mybatis分頁那點事

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);
    ////判定當前是否讀取是否在limitwhile (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兩種分頁方式,分別代表了邏輯分頁和物理分頁;以及這兩種方式是內部是如何實現的;最後文末做了一個簡單的效能測試。

示例程式碼

Github