为什么PageHelper能实现分页?

今天处理分页查询时,报PersistenceException。查看控制台发现,sql中没有获取到分页参数,而且多了一个用来查询总数queryUpfile_Count方法。

1 (Small).png

反复确认了传参无问题后,最后查看xml发现sql语句有拼接limit #{pageNo}, #{pageSize}。这时候我就想,原有项目中使用PageHelper做分页,是不是凭借的sql语句与PageHelper产生冲突呢?带着这个疑问我就来查看PageHelper的源码。

<!-- 查询上传文件记录 -->
<select id="queryUpfile" resultType="IotMsisdnUpfile">
  select <include refid="Base_Column_List"/>
  from iot_msisdn_upfile
  limit #{pageNo}, #{pageSize}; 
</select>

一般情况下我们使用PageHelper时,都会先使用startPage(),后面紧跟着一个查询语句,进而开始分页功能。

PageHelper.startPage(pageNo, pageSize);
List<IotMsisdnUpfile> list = iotMsisdnUpfileMapper.queryUpfile(pageNo, pageSize);

我们使用debug模式进入PageMethod中的startPage方法,一路往下点来到如下面所示的方法。发现我们传递pageNo和pageSize保存到Page中,并设置到setLocalPage(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;
}

点开setLocalPage()之后,恍然大悟,原来page保存到ThreadLocal中,用来保证变量的线程私有,确认线程安全问题。

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

/**
 * 设置 Page 参数
 *
 * @param page
 */
protected static void setLocalPage(Page page) {
    LOCAL_PAGE.set(page);
}

我们知道,ThreadLoacal中有set(),必然有get()。这时候我们就猜想MyBatis中肯定使用了get()方法获取到Page对象,并取出我们传入pageNo和pageSize,从而实现分页查询。我们继续往下面debug,来到MapperProxy中的invoke()方法,并且执行了execute()方法。

@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 (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

来到MapperMethod的execute()方法,我们发现这里使用switch来判断当前执行sql的类型。由于我们是SELECT语句而且返回多个结果,故来到了"SELECT"中的 executeForMany(sqlSession, args)。

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);
        result = sqlSession.selectOne(command.getName(), param);
      }
      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;
}

点开executeForMany(),发现其执行了一个selectList()方法

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
  List<E> result;
  Object param = method.convertArgsToSqlCommandParam(args);
  if (method.hasRowBounds()) {
    RowBounds rowBounds = method.extractRowBounds(args);
    result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
  } else {
    result = sqlSession.<E>selectList(command.getName(), param);
  }
  // issue #510 Collections & arrays support
  if (!method.getReturnType().isAssignableFrom(result.getClass())) {
    if (method.getReturnType().isArray()) {
      return convertToArray(result);
    } else {
      return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
    }
  }
  return result;
}

点开selectList()之后,我们就来到DefaultSqlSession。发现其返回executor.query()的结果,我们就知道了线程池连接数据库查询的方法就是executor.query()方法。

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

继续点开我们发现executor.query()被代理了,来到Plugin类中invoke()方法。仔细查看invoke()方法发现,这里执行了interceptor.intercept(new Invocation(target, method, args)),返回了一个拦截器。

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

点开intercept()来到了PageInterceptor中,这是我们才发现PageHelper就是通过实现MyBatis拦截器的模式来分页功能。这时我们进入PageInterceptor.intercept()发现如下代码,dialect.skip(ms, parameter, rowBounds)来判断是否需要分页。

//调用方法判断是否需要进行分页,如果不需要,直接返回结果
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);
}

我们这时进入skip()方法,这时眼睛一亮,终于找到PageHelper.getLocalPage()来获取我们Page对象,并一同返回给拦截器执行我们的查询。

@Override
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
    if (ms.getId().endsWith(MSUtils.COUNT)) {
        throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
    }
    Page page = pageParams.getPage(parameterObject, rowBounds);
    if (page == null) {
        return true;
    } else {
        //设置默认的 count 列
        if (StringUtil.isEmpty(page.getCountColumn())) {
            page.setCountColumn(pageParams.getCountColumn());
        }
        autoDialect.initDelegateDialect(ms);
        return false;
    }
}

/**
 * 获取分页参数
 *
 * @param parameterObject
 * @param rowBounds
 * @return
 */
public Page getPage(Object parameterObject, RowBounds rowBounds) {
    Page page = PageHelper.getLocalPage();
    if (page == null) {
        if (rowBounds != RowBounds.DEFAULT) {
            if (offsetAsPageNum) {
                page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
            } else {
                page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
                //offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false
                page.setReasonable(false);
            }
            if(rowBounds instanceof PageRowBounds){
                PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
                page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
            }
        } else if(parameterObject instanceof IPage || supportMethodsArguments){
            try {
                page = PageObjectUtil.getPageFromObject(parameterObject, false);
            } catch (Exception e) {
                return null;
            }
        }
        if(page == null){
            return null;
        }
        PageHelper.setLocalPage(page);
    }
    //分页合理化
    if (page.getReasonable() == null) {
        page.setReasonable(reasonable);
    }
    //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
    if (page.getPageSizeZero() == null) {
        page.setPageSizeZero(pageSizeZero);
    }
    return page;
}

最后,我们知道PageHelper已经帮我封装了pageNo和pageSize,所以我们就可以不用在sql语句中加入limit。所以,我们将sql修改如下,就可以愉快使用PageHelper来实现分页查询。

<!-- 查询上传文件记录 -->2
<select id="queryUpfile" resultType="IotMsisdnUpfile">
    select <include refid="Base_Column_List"/> from iot_msisdn_upfile
</select>
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容