1. 程式人生 > >淺析pagehelper分頁原理

淺析pagehelper分頁原理

原文連結 https://blog.csdn.net/qq_21996541/article/details/79796117

 

之前專案一直使用的是普元框架,最近公司專案搭建了新框架,主要是由公司的大佬搭建的,以springboot為基礎。為了多學習點東西,我也模仿他搭了一套自己的框架,但是在完成分頁功能的時候,確遇到了問題。

框架的分頁元件使用的是pagehelper,對其我也是早有耳聞,但是也是第一次接觸(ps:工作1年,一直使用的是普元封裝好的前端框架)。

要是用pagehelper,首先maven專案,要引入

<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper</artifactId>
   <version>4.1.6</version>
</dependency>


前端使用的bootstrap分頁外掛,這裡不再贅述,直接切入正題,持久層框架使用的是mybatis,分頁外掛的URL,指向後臺的controller,

@ResponseBody
@RequestMapping("/testPage")
public String testPage(HttpServletRequest request) {
   Page<PmProduct> page = PageUtil.pageStart(request);
   List<PmProduct> list = prodMapper.selectAll();
   JSONObject rst = PageUtil.pageEnd(request, page, list);
   return rst.toString();
}

這是controller的程式碼,當時就產生一個疑問,為啥在這個pageStart後面查詢就能夠實現分頁呢?可以查出想要的10條分頁資料,而去掉則是全部查詢出來的84條記錄。

帶著問題,我開始了我的debug之旅,首先,我跟進pageStart方法,這個PageUtil類是公司大佬封裝的一個工具類,程式碼如下:

public class PageUtil {

    public enum CNT{
        total,
        res
    }

    public static <E> Page<E> pageStart(HttpServletRequest request){
        return pageStart(request,null);
    }

    /**
     *
     * @param request
     * @param pageSize 每頁顯示條數
     * @param orderBy 寫入 需要排序的 欄位名 如: product_id desc
     * @return
     */
    public static <E> Page<E> pageStart(HttpServletRequest request,String orderBy){

        int pageon=getPageon(request);
        int pageSize=getpageSize(request);
        Page<E> page=PageHelper.startPage(pageon, pageSize);
        if(!StringUtils.isEmpty(orderBy)){
            PageHelper.orderBy(orderBy);
        }

        return page;
    }

    private static int getPageon(HttpServletRequest request){
        String pageonStr=request.getParameter("pageon");
        int pageon;
        if(StringUtils.isEmpty(pageonStr)){
            pageon=1;
        }else{
            pageon=Integer.parseInt(pageonStr);
        }
        return pageon;
    }

    private static int getpageSize(HttpServletRequest request){
        String pageSizeStr=request.getParameter("pageSize");
        int pageSize;
        if(StringUtils.isEmpty(pageSizeStr)){
            pageSize=1;
        }else{
            pageSize=Integer.parseInt(pageSizeStr);
        }
        return pageSize;
    }

    /**
     *
     * @param request
     * @param page
     * @param list
     * @param elName 頁面顯示所引用的變數名
     */
    public static JSONObject pageEnd(HttpServletRequest request, Page<?> page,List<?> list){
        JSONObject rstPage=new JSONObject();
        rstPage.put(CNT.total.toString(), page.getTotal());
        rstPage.put(CNT.res.toString(), list);
        return rstPage;
    }


}

可以看到,pageStart有兩個方法過載,進入方法後,獲取了前端頁面傳遞的pageon、pageSize兩個引數,分別表示當前頁面和每頁顯示多少條,然後呼叫了PageHelper.startPage,接著跟進此方法,發現也是一對方法過載,沒關係,往下看

/**
 * 開始分頁
 *
 * @param pageNum      頁碼
 * @param pageSize     每頁顯示數量
 * @param count        是否進行count查詢
 * @param reasonable   分頁合理化,null時用預設配置
 * @param pageSizeZero true且pageSize=0時返回全部結果,false時分頁,null時用預設配置
 */
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page<E>(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    //當已經執行過orderBy的時候
    Page<E> oldPage = SqlUtil.getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    SqlUtil.setLocalPage(page);
    return page;
}

上面的方法才是真正分頁呼叫的地方,原來是對傳入引數的賦值,賦給Page這個類,繼續,發現getLocalPage和setLoaclPage這兩個方法,很可疑,跟進,看看他到底做了啥,

public static <T> Page<T> getLocalPage() {
    return LOCAL_PAGE.get();
}

public static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

哦,有點明白了,原來就是賦值儲存到本地執行緒變數裡面,這個ThreadLocal是何方神聖,居然有這麼厲害,所以查閱了相關部落格,連結:https://blog.csdn.net/u013521220/article/details/73604917,個人簡單理解大概意思是這個類有4個方法,set,get,remove,initialValue,可以使每個執行緒獨立開來,引數互不影響,裡面儲存當前執行緒的變數副本。

OK,那這個地方就是儲存了當前分頁執行緒的Page引數的變數。有賦值就有取值,那麼在下面的分頁過程中,肯定在哪邊取到了這個threadLocal的page引數。

好,執行完startPage,下面就是執行了mybatis的SQL語句,

<select id="selectAll" resultMap="BaseResultMap">
  select SEQ_ID, PRODUCT_ID, PRODUCT_NAME, PRODUCT_DESC, CREATE_TIME, EFFECT_TIME,
  EXPIRE_TIME, PRODUCT_STATUS, PROVINCE_CODE, REGION_CODE, CHANGE_TIME, OP_OPERATOR_ID,
  PRODUCT_SYSTEM, PRODUCT_CODE
  from PM_PRODUCT
</select>

SQL語句很簡單,就是簡單的查詢出PM_PRODUCT的全部記錄,那到底是哪邊做了攔截嗎?帶著這個疑問,我跟進了程式碼,

發現進入了mybatis的MapperPoxy這個代理類,

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if (Object.class.equals(method.getDeclaringClass())) {
    try {
      return method.invoke(this, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

最後執行的execute方法,再次跟進,進入MapperMethod這個類的execute方法,

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  if (SqlCommandType.INSERT == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.insert(command.getName(), param));
  } else if (SqlCommandType.UPDATE == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.update(command.getName(), param));
  } else if (SqlCommandType.DELETE == command.getType()) {
    Object param = method.convertArgsToSqlCommandParam(args);
    result = rowCountResult(sqlSession.delete(command.getName(), param));
  } else if (SqlCommandType.SELECT == command.getType()) {
    if (method.returnsVoid() && method.hasResultHandler()) {
      executeWithResultHandler(sqlSession, args);
      result = null;
    } else if (method.returnsMany()) {
      result = executeForMany(sqlSession, args);
    } else if (method.returnsMap()) {
      result = executeForMap(sqlSession, args);
    } else if (method.returnsCursor()) {
      result = executeForCursor(sqlSession, args);
    } else {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = sqlSession.selectOne(command.getName(), param);
    }
  } else if (SqlCommandType.FLUSH == command.getType()) {
      result = sqlSession.flushStatements();
  } else {
    throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

由於執行的是select操作,並且查詢出多條,所以就到了executeForMany這個方法中,後面繼續跟進程式碼SqlSessionTemplate,DefaultSqlSession(不再贅述),最後可以看到程式碼進入了Plugin這個類的invoke方法中,

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

這下明白了,interceptor是mybatis的攔截器,而PageHelper這個類就實現了interceptor介面,呼叫其中的intercept方法。

/**
 * Mybatis攔截器方法
 *
 * @param invocation 攔截器入參
 * @return 返回執行結果
 * @throws Throwable 丟擲異常
 */
public Object intercept(Invocation invocation) throws Throwable {
    if (autoRuntimeDialect) {
        SqlUtil sqlUtil = getSqlUtil(invocation);
        return sqlUtil.processPage(invocation);
    } else {
        if (autoDialect) {
            initSqlUtil(invocation);
        }
        return sqlUtil.processPage(invocation);
    }
}

/**
 * Mybatis攔截器方法
 *
 * @param invocation 攔截器入參
 * @return 返回執行結果
 * @throws Throwable 丟擲異常
 */
private Object _processPage(Invocation invocation) throws Throwable {
    final Object[] args = invocation.getArgs();
    Page page = null;
    //支援方法引數時,會先嚐試獲取Page
    if (supportMethodsArguments) {
        page = getPage(args);
    }
    //分頁資訊
    RowBounds rowBounds = (RowBounds) args[2];
    //支援方法引數時,如果page == null就說明沒有分頁條件,不需要分頁查詢
    if ((supportMethodsArguments && page == null)
            //當不支援分頁引數時,判斷LocalPage和RowBounds判斷是否需要分頁
            || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
        return invocation.proceed();
    } else {
        //不支援分頁引數時,page==null,這裡需要獲取
        if (!supportMethodsArguments && page == null) {
            page = getPage(args);
        }
        return doProcessPage(invocation, page, args);
    }
}

最終我在SqlUtil中的_processPage方法中找到了,getPage這句話,getLocalPage就將儲存在ThreadLocal中的Page變數取了出來,這下一切一目瞭然了,


跟進程式碼,發現進入了doProcessPage方法,通過反射機制,首先查詢出資料總數量,然後進行分頁SQL的拼裝,MappedStatement的getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }

  // check for nested result maps in parameter mappings (issue #30)
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }

  return boundSql;
}

繼續,跟進程式碼,發現,最終分頁的查詢,調到了PageStaticSqlSource類的getPageBoundSql中,

protected BoundSql getPageBoundSql(Object parameterObject) {
    String tempSql = sql;
    String orderBy = PageHelper.getOrderBy();
    if (orderBy != null) {
        tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
    }
    tempSql = localParser.get().getPageSql(tempSql);
    return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
}

進入getPageSql這個方法,發現,進入了OracleParser類中(還有很多其他的Parser,適用於不同的資料庫),

public String getPageSql(String sql) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
    sqlBuilder.append("select * from ( select tmp_page.*, rownum row_id from ( ");
    sqlBuilder.append(sql);
    sqlBuilder.append(" ) tmp_page where rownum <= ? ) where row_id > ?");
    return sqlBuilder.toString();
}

終於,原來分頁的SQL是在這裡拼裝起來的。

總結:PageHelper首先將前端傳遞的引數儲存到page這個物件中,接著將page的副本存放入ThreadLoacl中,這樣可以保證分頁的時候,引數互不影響,接著利用了mybatis提供的攔截器,取得ThreadLocal的值,重新拼裝分頁SQL,完成分頁。