因为一个bug来深入探讨下分页插件PageHelper

2023-11-15

事情来源是这样的,因为某些操作失误,在使用分页插件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对象

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

因为一个bug来深入探讨下分页插件PageHelper 的相关文章

随机推荐

  • 多案分库分表带来的问题-跨库关联查询

    比如查询在合同信息的时候要关联客户数据 由于是合同数据和客户数据是在不同的数据库 那么我们肯定不能直接使用join 的这种方式去做关联查询 我们有几种主要的解决方案 1 字段冗余 比如我们查询合同库的合同表的时候需要关联客户库的客户表 我们
  • MPQ8633性能指标测试与调测分享

    目录 基于MPQ8633A测试分析 1 开关电源的三种基本拓扑结构 1 1 buck电路的基本拓扑以及原理 2 CCM DCM BCM 概论 2 1 PWM PFM PSM三种控制方法的优缺点 2 2 DCDC BUCK各种模式 2 3 C
  • 使用easypoi模板方法导出excel

    系列文章目录 一 Java使用Apache POI导出excel 二 Apache POI 操作Excel常用方法 三 Apache poi 拆分单元格并赋值 四 使用easypoi模板方法导出excel 五 Apache poi给exce
  • Dynamics 365 多选字段

    Dynamics 365 多选字段 数据库操作 C 后台操作 JS 扩展 JS string 转 int数组 数据库操作 update tablebase set field 1 对应选项值多个逗号隔开 1 C 后台操作
  • Testng学习笔记

    TestNG设计涵盖所有类型的测试 单元 功能 端到端 集成等 它需要JDK5或更高的JDK版本 TestNG是一个开源自动化测试框架 其灵感来自JUnit和NUnit的 TestNG环境设置 配置安装 TestNG是一个Java的框架 所
  • 在浏览器地址栏输入一个网址,从敲下回车键,到页面完全加载完毕,中间都发生了什么?

    1 如果地址栏中输入的是一个域名 浏览器会先使用自己的DNS缓存进行域名解析 转为IP地址 如果缓存中不存在 则会请求使用上层DNS 操作系统的DNS 操作系统会先查询本地HOST文件 如果HOST文件中不存在则会使用网络设置的DNS进行域
  • 关于 Q值

    先从大的方面来讲 从系统 从能量的角度来讲 Q值 描述任何可储能器件的性能 比如LC振荡回路 激光谐振腔 FP标准具 即Q 系统内能量 损耗功率 可见 Q值是储能器件受 振 荡驱动力和阻尼力的矛盾作用的结果 Q值越高 表示阻尼越小 当Q值
  • unity ugui序列帧动画

    使用unity自带的动画功能 1 使用unity自带的动画功能 2 在脚本中不停切换一个UI的图片以实现动画效果 1 使用unity自带的动画功能 2 在脚本中不停切换一个UI的图片以实现动画效果 using UnityEngine usi
  • 【配电网故障重构SOP】基于二阶锥松弛的加光伏风机储能进行的配电网故障处理和重构【考虑最优潮流】(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Matlab代码实现 1 概述 电力系统最优潮流 综合考虑电网安全性和经济
  • c++哈希表-map&set (无序)

    简述 无序的map set 称为unordered map unordered set 采用迭代器遍历出来的元素是无序的 这是因此底层实现数据结构为哈希表 1 哈希表不同于红黑树 哈希表它的查找效率是o 1 一个常数的效率 虽然红黑树是o
  • 使用的chorm插件 留存自用

    使用的chorm插件 留存自用 保存一下常用的插件 省的以后忘记了 啊哈哈 掘金插件 每次打开浏览器都得看上几眼 JSON handle 在线json解析 不解释了 好用 PostMan 模拟api的网络请求 The Great Suspe
  • 浅谈ThreadLocal

    目录 一 ThreadLocal原理 二 TheadLocal特性 二 TheadLocal使用场景 一 ThreadLocal原理 1 每个Thread对象都含有一个ThreadLocalMap ThreadLocalMap是由Entry
  • node.js错误 --> node-pre-gyp install --fallback-to-build

    错误 node pre gyp install fallback to build 问题描述 在安装ghost博客的时候需要使用node js 但是在执行sudo npm install production的时候报了这个错误 接着百度 看
  • 饥饿和公平

    转自 饥饿和公平 下文笔者将讲述java多线程中的 饥饿与公平 的相关知识 如下所示 Java中饥饿与公平的概念 当一个线程因为CPU时间全部被其他线程获取而无法获取CPU运行时间时 我们将这种状态称之为 饥饿 此时此线程由于 饥饿 而无法
  • 最大公约数与最小公倍数 -- 3种计算方法详解

    目录 1 穷举法 最大公约数 思路 最小公倍数 思路 2 辗转相除法 最大公约数与最小公倍数 最大公约数思路 最小公倍数思路 公式 3 特殊方法 最大公约数 相减法 思路 最小公倍数 叠乘法 思路 总结 1 穷举法 最大公约数 思路 假设两
  • C/C++问题处理:error : no matching function for call to ‘max‘

    报错代码 MaxVal max MaxVal words i size words j size 上面的 words 是vector
  • vue的双向绑定原理---defineProperty->Proxy

    可以实现双向绑定的方法有很多 基于观察者模式 基于数据模型 Angular基于脏检查的双向绑定 本篇我们重点讲vue的基于数据劫持和发布订阅的双向绑定 vue实例化过程中进行了以下操作 initLifecycle vm initEvents
  • Android性能之内存泄漏

    和你一起终身学习 这里是程序员Android 经典好文推荐 通过阅读本文 您将收获以下知识点 一 什么是内存泄漏 二 Android中导致内存泄漏的主要几个点三 Java虚拟机内存管理四 Java内存几种分配策略 五 垃圾收集器是如何判断对
  • MATLAB环境下基于支持向量机、孤立森林和LSTM自编码器的三轴振动数据的机械异常检测

    本文讲解如何使用机器学习和深度学习来检测机械振动数据中的异常 属于异常检测领域 这玩意在工业中用的更多 因为工业中毕竟还是正常样本远多于故障样本 异常样本 运行环境为MATLAB R2021B 第三方面包多官网下载如下 正在为您运送作品详情
  • 因为一个bug来深入探讨下分页插件PageHelper

    事情来源是这样的 因为某些操作失误 在使用分页插件pageHelper时 因为这样一句不起眼的操作 竟然引发了一系列的灾难 下面来看下灾难的由来 Page localPage PageHelper startPage page limit