事情来源是这样的,因为某些操作失误,在使用分页插件pageHelper时,因为这样一句不起眼的操作,竟然引发了一系列的灾难,下面来看下灾难的由来:
Page localPage = PageHelper.startPage(page, limit);注意参数 page和limit
我的失误呢是因为参数page是0,导致mybatis层在执行sql分页查询的时候,只做了count统计查询总数 ,并没有去执行mapper实际查询语句,也是导致selectList查询语句查询的结果是空,于是我深入了下源码进行了分析,下面就一起来进入源码进行分析。
PageHelper
class PageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain
我们经常使用的PageHelper.startPage(page, limit);是PageMethod的方法,它的主要方法是
/** * 开始分页 * * @param pageNum 页码 * @param pageSize 每页显示数量 */public static <E> Page<E> startPage(int pageNum, int pageSize) { return startPage(pageNum, pageSize, DEFAULT_COUNT); }
最终执行的分页
/** * 开始分页 * * @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; }
可以看出来pageHelper源码还是很不错的,是中文注释,这也要感谢大牛们的付出。
上面的 startPage静态方法一看就很容易明白,上面注解也很清晰,就是初始化记录几个变量,也就是下面的几个变量
Page类
/** * 页码,从1开始 */ private int pageNum; /** * 页面大小 */ private int pageSize; /** * 包含count查询 这个参数是用来表示是否执行count统计查询的,如果设置为false则不会执行查询总数 */ private boolean count = true; /** * 分页合理化 value=true时,pageNum小于1会查询第一页,如果pageNum大于pageSize会查询最后一页 ,个人认为,参数校验在进入Mybatis业务体系之前,就应该完成了,不可能到达Mybatis业务体系内参数还带有非法的值 */ private Boolean reasonable; /** * 当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果 */ private Boolean pageSizeZero; /** * 排序 */ private String orderBy; /** * 只增加排序 */ private boolean orderByOnly;
protected static boolean DEFAULT_COUNT = true;
继续聊PageMethod类,它还要其他的分页方法
/** * 开始分页 * * @param offset 起始位置,偏移位置 * @param limit 每页显示数量 */ public static <E> Page<E> offsetPage(int offset, int limit) { return offsetPage(offset, limit, DEFAULT_COUNT); }/** * 开始分页 * * @param offset 起始位置,偏移位置 * @param limit 每页显示数量 * @param count 是否进行count查询 */ public static <E> Page<E> offsetPage(int offset, int limit, boolean count) { Page<E> page = new Page<E>(new int[]{offset, limit}, count); //当已经执行过orderBy的时候 Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; }
与前面页数不同的是这里表示的是偏移量,在Page里的方法是下面这样的
/** * int[] rowBounds * 0 : offset * 1 : limit */ 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]; }
这里注意下Page类是基础了ArrayList的,这样表面了可以直接将查询出的List集合结果转换为Page实体类
public class Page<E> extends ArrayList<E> implements Closeable
稍作了解Page类的doSelect方法和toPageInfo方法
ISelect 是个接口自己可以去实现doSelect方法
public <E> Page<E> doSelectPage(ISelect select) { select.doSelect(); return (Page<E>) this; }public <T> PageInfo<T> toPageInfo(Function<E, T> function) { List<T> list = new ArrayList<T>(this.size()); for (E e : this) { list.add(function.apply(e)); } PageInfo<T> pageInfo = new PageInfo<T>(list); pageInfo.setPageNum(this.getPageNum()); pageInfo.setPageSize(this.getPageSize()); pageInfo.setPages(this.getPages()); pageInfo.setStartRow(this.getStartRow()); pageInfo.setEndRow(this.getEndRow()); pageInfo.calcByNavigatePages(PageInfo.DEFAULT_NAVIGATE_PAGES); return pageInfo; }
PageInfo类
/** * 包装Page对象 * * @param list page结果 * @param navigatePages 页码数量 */ public PageInfo(List<T> list, int navigatePages) { super(list); if (list instanceof Page) { Page page = (Page) list; this.pageNum = page.getPageNum(); this.pageSize = page.getPageSize(); this.pages = page.getPages(); this.size = page.size(); //由于结果是>startRow的,所以实际的需要+1 if (this.size == 0) { this.startRow = 0; this.endRow = 0; } else { this.startRow = page.getStartRow() + 1; //计算实际的endRow(最后一页的时候特殊) this.endRow = this.startRow - 1 + this.size; } } else if (list instanceof Collection) { this.pageNum = 1; this.pageSize = list.size(); this.pages = this.pageSize > 0 ? 1 : 0; this.size = list.size(); this.startRow = 0; this.endRow = list.size() > 0 ? list.size() - 1 : 0; } if (list instanceof Collection) { calcByNavigatePages(navigatePages); } }直接完成将List结果数据集合封装为分页对象,实际开发中也可以按照这种格式来自定义自己的分页查询结果
上面了解了Page,PageHelp ,Page ,PageMethod,PageInfo这几个类,下面来具体谈谈PagHelper分页插件是如何拆分Sql并重装为分页查询,也就是我们常见的SQl语句
select * from table
where
查询条件
order
排序
limit pageNum pageSize
PageInterceptor
PageInterceptor这个拦截类,就是来完成sql拼装的,来看下它具体是如何工作的
public class PageInterceptor implements Interceptor首先它是实现了Interceptor拦截器接口的,这个接口是mybatis自带的接口,为方便实现自定义拦截器
package org.apache.ibatis.plugin; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; default Object plugin(Object target) { return Plugin.wrap(target, this); } default void setProperties(Properties properties) { // NOP } }
PageInteceptor拦截器,主要方法public Object intercept(Invocation invocation) throws Throwable
public Object intercept(Invocation invocation) throws Throwable { try { //获取参数 拦截的 mapper方法 Object[] args = invocation.getArgs(); //根据名字就知道是获取mapperStatment MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; //sql 分页查询参数 RowBounds rowBounds = (RowBounds) args[2]; ResultHandler resultHandler = (ResultHandler) args[3]; //执行器 mybatis执行器 Executor executor = (Executor) invocation.getTarget(); //缓存Key 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]; } //这个检查dialect 默认dialect "com.github.pagehelper.PageHelper" 如果你自己实现了 Dialect 接口也可以自定义数据库方言 checkDialectExists(); //对 boundSql 的拦截处理 if (dialect instanceof BoundSqlInterceptor.Chain) { boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey); } List resultList; //调用方法判断是否需要进行分页,如果不需要,直接返回结果 if (!dialect.skip(ms, parameter, rowBounds)) { //判断是否需要进行 count 查询 if (dialect.beforeCount(ms, parameter, rowBounds)) { //查询总数 Long count = count(executor, ms, parameter, rowBounds, null, 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 该方法的返回值 这个结果就是查询分页Page分页查询结果 包含查询统计总数 以及当前页获取记录 return dialect.afterPage(resultList, parameter, rowBounds); } finally { //如果分页查询,会移除本地分页 if(dialect != null){ dialect.afterAll(); } } }
count方法查询总数
private Long count(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { String countMsId = ms.getId() + countSuffix; Long count; //先判断是否存在手写的 count 查询 MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId); if (countMs != null) { count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler); } else { if (msCountMap != null) { countMs = msCountMap.get(countMsId); } //自动创建 if (countMs == null) { //根据当前的 ms 创建一个返回值为 Long 类型的 ms countMs = MSUtils.newCountMappedStatement(ms, countMsId); if (msCountMap != null) { msCountMap.put(countMsId, countMs); } } count = ExecutorUtil.executeAutoCount(this.dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler); } return count; }
这一块逻辑自己可以deubg去查看下具体的执行逻辑顺序,通过查看参数可以更加明确
分页查询ExecutorUtil类 pageQuery 方法
/** * 分页查询 * * @param dialect * @param executor * @param ms * @param parameter * @param rowBounds * @param resultHandler * @param boundSql * @param cacheKey * @param <E> * @return * @throws SQLException */ 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)); } //对 boundSql 的拦截处理 if (dialect instanceof BoundSqlInterceptor.Chain) { pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey); } //执行分页查询 return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //不执行分页的情况下,也不执行内存分页 return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } }
Dialect的调用方言获取SQl接口
/** * 生成分页查询 sql * * @param ms MappedStatement * @param boundSql 绑定 SQL 对象 * @param parameterObject 方法参数 * @param rowBounds 分页参数 * @param pageKey 分页缓存 key * @return */ String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
在PageHelper中会实现分页查询方法,返回拼装的分页查询sql
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); }@Override public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) { Page page = getLocalPage(); if (page == null) { return pageList; } page.addAll(pageList); if (!page.isCount()) { page.setTotal(-1); } else if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) { page.setTotal(pageList.size()); } else if (page.isOrderByOnly()) { page.setTotal(pageList.size()); } return page; }
以上流程就是PageHelper分页查询流程,其中PageInterceptor主要完成了对分页查询统计计数以及对分页查询sql拼装和执行,执行完成后会把查询统计结果封装为Page对象