1. 程式人生 > 實用技巧 >若依專案中如何分頁的-淺析

若依專案中如何分頁的-淺析

使用的外掛有

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper-spring-boot-starter</artifactId>
  <version>1.2.13</version>
</dependency>

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.3.1</version>
</dependency>

控制器中使用下面語句就能分頁。

  startPage();
  List<SysUser> list = userService.selectUserList(user);
  return getDataTable(list);

方法startPage,有什麼用呢?

protected void startPage()
{
    PageDomain pageDomain = TableSupport.buildPageRequest();
    Integer pageNum = pageDomain.getPageNum();
    Integer pageSize = pageDomain.getPageSize();
    if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
    {
        String orderBy = pageDomain.getOrderBy();
        PageHelper.startPage(pageNum, pageSize, orderBy);
    }
}

startPage首先將前臺傳遞過來的分頁資訊記錄下來。

public static PageDomain getPageDomain()
{
    PageDomain pageDomain = new PageDomain();
    pageDomain.setPageNum(ServletUtils.getParameterToInt(Constants.PAGE_NUM));
    pageDomain.setPageSize(ServletUtils.getParameterToInt(Constants.PAGE_SIZE));
    pageDomain.setOrderByColumn(ServletUtils.getParameter(Constants.ORDER_BY_COLUMN));
    pageDomain.setIsAsc(ServletUtils.getParameter(Constants.IS_ASC));
    return pageDomain;
}

public static PageDomain buildPageRequest()
{
    return getPageDomain();
}

那麼PageHelper.startPage(pageNum, pageSize, orderBy)是什麼呢?

/**
  * 開始分頁
  *
  * @param pageNum  頁碼
  * @param pageSize 每頁顯示數量
  * @param orderBy  排序
  */
public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {
    Page<E> page = startPage(pageNum, pageSize);
    page.setOrderBy(orderBy);
    return page;
}
···
/**
  * 開始分頁
  *
  * @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 = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }
    setLocalPage(page);
    return page;
}

其中getLocalPage()和setLocalPage();很奇怪:

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static boolean DEFAULT_COUNT = true;

/**
  * 設定 Page 引數
  *
  * @param page
  */
protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

/**
  * 獲取 Page 引數
  *
  * @return
  */
public static <T> Page<T> getLocalPage() {
    return LOCAL_PAGE.get();
}

原來setLocalPage是將page儲存到本地執行緒變數裡面。那麼有賦值,就該有取值,不然就沒什麼用了,對吧,但是在哪裡用呢?先跳過這裡。回到selectUserList。
執行完startPage,就執行了mybatis的sql語句:

<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.login_name, u.user_name, u.email, u.phonenumber, u.password, u.sex, u.avatar, u.salt, u.status, u.order_num, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.update_time, u.remark
from sys_user u
where u.del_flag = '0'
group by u.user_id
order by u.order_num, u.user_name, d.order_num
</select>

就是這麼簡單的sql語句,為甚頁面會分頁呢?難道有地方進行了攔截處理?

debug發現進入com.baomidou.mybatisplus.core.override的MybatisMapperProxy類中

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      try {
          if (Object.class.equals(method.getDeclaringClass())) {
              return method.invoke(this, args);
          } else if (method.isDefault()) {
              if (privateLookupInMethod == null) {
                  return invokeDefaultMethodJava8(proxy, method, args);
              } else {
                  return invokeDefaultMethodJava9(proxy, method, args);
              }
          }
      } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
      }
      final MybatisMapperMethod mapperMethod = cachedMapperMethod(method);
      return mapperMethod.execute(sqlSession, args);
  }

最後執行execute方法,接著進入MybatisMapperMethod類中:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
      case INSERT: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.insert(command.getName(), param));
          break;
      }
      case UPDATE: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.update(command.getName(), param));
          break;
      }
      case DELETE: {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = rowCountResult(sqlSession.delete(command.getName(), param));
          break;
      }
      case SELECT:
          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);
              if (IPage.class.isAssignableFrom(method.getReturnType())) {
                  assert args != null;
                  IPage<?> page = null;
                  for (Object arg : args) {
                      if (arg instanceof IPage) {
                          page = (IPage) arg;
                          break;
                      }
                  }
                  assert page != null;
                  result = executeForIPage(sqlSession, args);
                  if (result instanceof PageList) {
                      PageList pageList = (PageList) result;
                      page.setRecords(pageList.getRecords());
                      page.setTotal(pageList.getTotal());
                      result = page;
                  } else {
                      List list = (List<Object>) result;
                      result = page.setRecords(list);
                  }
              } else {
                  result = sqlSession.selectOne(command.getName(), param);
                  if (method.returnsOptional()
                      && (result == null || !method.getReturnType().equals(result.getClass()))) {
                      result = Optional.ofNullable(result);
                  }
              }
          }
          break;
      case FLUSH:
          result = sqlSession.flushStatements();
          break;
      default:
          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,所以到了 result = executeForMany(sqlSession, args)
然後是SqlSessionTemplate和DefaultSqlSession
最後可以看到程式碼進入了org.apache.ibatis.plugin.Plugin這個類的invoke方法中

  @Override
  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的攔截器,而com.github.pagehelper.PageInterceptor這個類就實現了interceptor介面,呼叫其中的intercept方法。

@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        //由於邏輯關係,只會進入一次
        if (args.length == 4) {
            //4 個引數時
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            //6 個引數時
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        checkDialectExists();

        List resultList;
        //呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果
        if (!dialect.skip(ms, parameter, rowBounds)) {
            //判斷是否需要進行 count 查詢
            if (dialect.beforeCount(ms, parameter, rowBounds)) {
                //查詢總數
                Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                //處理查詢總數,返回 true 時繼續分頁查詢,false 時直接返回
                if (!dialect.afterCount(count, parameter, rowBounds)) {
                    //當查詢總數為 0 時,直接返回空的結果
                    return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                }
            }
            resultList = ExecutorUtil.pageQuery(dialect, executor,
                    ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            //rowBounds用引數值,不使用分頁外掛處理時,仍然支援預設的記憶體分頁
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if(dialect != null){
            dialect.afterAll();
        }
    }
}

ExecutorUtil.pageQuery()來處理分頁,但是怎麼處理的呢?
com.github.pagehelper.util.ExecutorUtil

public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                 RowBounds rowBounds, ResultHandler resultHandler,
                                 BoundSql boundSql, CacheKey cacheKey) throws SQLException {
    //判斷是否需要進行分頁查詢
    if (dialect.beforePage(ms, parameter, rowBounds)) {
        //生成分頁的快取 key
        CacheKey pageKey = cacheKey;
        //處理引數物件
        parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
        //呼叫方言獲取分頁 sql
        String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        //設定動態引數
        for (String key : additionalParameters.keySet()) {
            pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
        //執行分頁查詢
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
    } else {
        //不執行分頁的情況下,也不執行記憶體分頁
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
    }
}

getPageSql方法:
com.github.pagehelper.PageHelper

@Override
  public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
      return autoDialect.getDelegate().getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
  }

然後呼叫com.github.pagehelper.dialect.AbstractHelperDialect

@Override
  public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
      String sql = boundSql.getSql();
      Page page = getLocalPage();
      //支援 order by
      String orderBy = page.getOrderBy();
      if (StringUtil.isNotEmpty(orderBy)) {
          pageKey.update(orderBy);
          sql = OrderByParser.converToOrderBySql(sql, orderBy);
      }
      if (page.isOrderByOnly()) {
          return sql;
      }
      return getPageSql(sql, page, pageKey);
  }

在getPageSql中getLocalPage將ThreadLocal中page變數取出來。
com.github.pagehelper.dialect.helper.MySqlDialect

@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    if (page.getStartRow() == 0) {
        sqlBuilder.append(" LIMIT ? ");
    } else {
        sqlBuilder.append(" LIMIT ?, ? ");
    }
    return sqlBuilder.toString();
}

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