Mybatis分页插件之PageHelper生效and失效原理解析

文章目录

    • 前言
    • 整合PageHelper
    • PageHelper生效原理
      • PageHelper的分页参数和线程绑定
      • 核心拦截逻辑
      • 生成分页SQL
      • dialect.afterAll()
    • PageHelper失效原理
      • 分页失效案例
      • 分页失效原理
      • 总结

Mybatis拦截器系列文章:
从零开始的 MyBatis 拦截器之旅:实战经验分享
构建自己的拦截器:深入理解MyBatis的拦截机制
Mybatis分页插件之PageHelper原理解析

在这里插入图片描述

前言

PageHelper是一个优秀的Mybatis分页插件,它可以帮助我们自动完成分页查询的工作。它的使用非常简单,只需要在查询之前调用PageHelper.startPage方法,传入页码和每页大小,就可以实现分页效果。PageHelper还提供了很多其他的配置和功能,例如排序、合理化、分页参数映射等。

那么,PageHelper是如何实现分页功能的呢?本文将从源码的角度,一步步分析PageHelper的实现原理,希望能够对大家有所帮助。

整合PageHelper

整合 PageHelper 并不难,先导入 PageHelper 的依赖:

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

之后给 MyBatis 配置上 PageHelper 的核心拦截器:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="typeAliasesPackage" value="com.apple.entity"/>
        <property name="plugins">
            <list>
                <bean class="com.github.pagehelper.PageInterceptor">
                    <property name="properties">
                        <props>
                            <prop key="helperDialect">mysql</prop>
                            <prop key="reasonable">true</prop>
                        </props>
                    </property>
                </bean>
            </list>
        </property>
    </bean>

之后在需要分页查询的位置前面,加上一句话:

PageHelper.startPage(1, 2);
List<Department> departmentList = departmentMapper.findAll();

这样运行的时候,PageHelper 就起作用了:

[main] DEBUG extra.DepartmentMapper.findAll  - ==>  Preparing: SELECT * FROM tbl_department WHERE (isdel = 0) LIMIT ? 
[main] DEBUG extra.DepartmentMapper.findAll  - ==> Parameters: 2(Integer) 
[main] DEBUG extra.DepartmentMapper.findAll  - <==      Total: 2

可以发现 SQL 的最后有 limit 的后缀,只查了两条数据。

PageHelper生效原理

它的基本原理是通过拦截Executor,StatementHandler,ParameterHandler和ResultSetHandler这四个对象,修改原始的sql语句,增加limit和count等语句,从而实现分页效果。

我们仅仅加上 PageHelper.startPage(1, 2); 这句代码,分页就生效了,那一定是这句代码的背后发生了重要的事情,我们可以跟进去看一下。

PageHelper的分页参数和线程绑定

public static <E> Page<E> startPage(int pageNum, int pageSize) {
    return startPage(pageNum, pageSize, DEFAULT_COUNT);
}

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
    return startPage(pageNum, pageSize, count, null, 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());
    }
     //将Page对象绑定到当前线程的局部变量中
    setLocalPage(page);
    return page;
}

自上而下调用直至最底下的方法,而最底下的方法中有一句代码我们要着重的去看:setLocalPage(page);

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

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

这句源码将当前的分页内容设置到了 ThreadLocal 中!那就意味着,当前线程的任意位置都能取到分页的两个参数了。

核心拦截逻辑

img

线程中有了分页的参数,下面执行到 PageHelper 的核心拦截器中,就可以顺势取出了,我们来到 PageInterceptor 中:

拦截方法主要做了两件事,一件执行countBoundsql获得count,一件执行pageBoundSql获得resultList。

在这里插入图片描述

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

大段的源码虽然长,但是 PageHelper 毕竟是我们自己人的产品,注释都是中文的看起来也友好的多。这里们我们最应该关注的动作,是中间偏下的,有方括号标注的那个静态方法调用:ExecutorUtil.pageQuery

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

注意这个 if 结构的逻辑,它是先生成一条带有分页片段的新 SQL ,之后封装为一个全新的 BoundSql ,交给 Executor 执行。所以我们可以总结为一点:分页插件的工作核心其实就是偷梁换柱!将原有的 SQL 替换为带分页语法的 SQL ,交给 Executor ,而 Executor 本身不会感知到,所以最后查询得到的就是分页之后的数据了。

另外调用方言获取分页 SQL 的动作,这句代码,会将原有的全表查询,修饰为分页片段查询,这是分页 SQL 的核心生成逻辑,我们一定要进去看看。

生成分页SQL

这个分页 SQL 的生成,又是体现着模板方法的设计了,我们进入 dialect.getPageSql 方法中:

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

上面支持 order by 语法的逻辑我们就不关心了,主要是来看最后一句 getPageSql 的实现,

这个方法本身是一个模板方法,它的实现有好多个:
在这里插入图片描述

而我们目前正在使用的 MySQL 的实现如下:

// MySqlDialect
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();
}

MySQL 的分页语法是非常简单的了,只需要拼接 limit 参数就 OK 。

走到这里,分页 SQL 也就生成了,分页查询也就随之进行了。

以上就是 PageHelper 的基本原理,可以发现本身不难,其实我们来实现也是完全没问题的,我们完全可以仿照着 PageHelper 的实现机制,自己动手写一个。

dialect.afterAll()

讲这个是为了说明理解的PageHelper为什么有时候会失效

在最开始的PageInterceptor的try finally代码块中,有个

try{...}
} finally {
        if(dialect != null){
            // 最终执行的是清空ThreadLocal<Page>操作,LOCAL_PAGE.remove()
            dialect.afterAll();
        }
    }

最终执行的是清空ThreadLocal< Page>操作,LOCAL_PAGE.remove()

    public void afterAll() {
        //这个方法即使不分页也会被执行,所以要判断 null
        AbstractHelperDialect delegate = autoDialect.getDelegate();
        if (delegate != null) {
            delegate.afterAll();
            autoDialect.clearDelegate();
        }
        clearPage();
    }

PageHelper失效原理

分页失效案例

如下代码所示: 执行后,我们会发现departmentList2查询出来的是全量查询,并没有分页

PageHelper.startPage(1, 2);
List<Department> departmentList = departmentMapper.findAll();
List<Department> departmentList2 = departmentMapper.findAll();

分页失效原理

从上面讲到的生效原理,我们可以知道:

  • 判断是否支持分页主要是根据能否存在ThreadLocal<Page>,如果没有则不进行分页操作。

  • 我们的每个mapper查询都会经过拦截器处理,拦截器处理的最后一步是dialect.afterAll(),最终执行的是LOCAL_PAGE.remove(),即移除本地变量。

  • 这也就是为什么案例中执行第一个mapper查询会按照指定页数和每页显示条数查询出对应分页数据(因为存在ThreadLocal<Page>),而第二个mapper查询的是所有(因为第一个mapper查询完成之后会将ThreadLocal<Page>进行清除)。

清楚原因之后如何处理就简单了。如果同一个方法中多个mapper都需要支持分页操作,那都保证每个mapper前面都进行ThreadLocal<Page>初始化赋值操作。修改后代码如下:

PageHelper.startPage(1, 2);
List<Department> departmentList = departmentMapper.findAll();
PageHelper.startPage(1, 2);
List<Department> departmentList2 = departmentMapper.findAll();

总结

对指定mapper查询支持分页,前面一定要有PageHelper.startPage(currentPage,pageSize),不能有其他mapper查询,否则会失效!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/281722.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

S32K312使用ITCM向FLASH代码区写入数据

使用C40_IP的系列方法向FLASH代码区写入数据时&#xff0c;程序会卡死在读取写操作的状态C40_Ip_MainInterfaceWriteStatus()这个方法中。本文主要介绍S32K312通过ITCM的方式&#xff0c;通过C40_IP的方法向FLASH代码区成功写入数据的方法和步骤。 首先&#xff0c;验证一下C4…

macos下php 5.6 7.0 7.4 8.0 8.3 8.4全版本PHP开发环境安装方法

在macos中如果使用brew 官方默认的core tap 只可以安装官方最新的稳定版PHP, 如果想要安装 php 5.6 或者 php 8.4版本的PHP就需要使用第三方的tap , 这里分享一个比较全面的brew tap shivammathur/php 这个tap里面包含了从php5.6到最新版php8.4的所有可用最新版本PHP, 而且是同…

python大于等于小于等于,python大于等于怎么写

大家好&#xff0c;小编为大家解答python中大于等于且小于等于的问题。很多人还不知道python大于号小于号如何运用&#xff0c;现在让我们一起来看看吧&#xff01; 大家好&#xff0c;小编来为大家解答以下问题&#xff0c;python中大于并小于一个数代码&#xff0c;python 大…

数据结构【线性表篇】(二)

数据结构【线性表篇】(二&#xff09; 文章目录 数据结构【线性表篇】(二&#xff09;前言为什么突然想学算法了&#xff1f;为什么选择码蹄集作为刷题软件&#xff1f; 目录一、单链表(一)、单链表的定义(二)、单链表的建立(三)、单链表的插入删除(四)、单链表的查找 二、主函…

springBoot2.3-基本介绍及入门案例

本次学习雷丰阳springBoot(2.3版本)。建议先修ssm 一、SpringBoot基本介绍 springBoot是当今最为流行的java开发框架。 1、springBoot的底层是spring&#xff0c; 因此继承了spring的粘合其他框架的能力。 2、本质上还是其他框架包括spring在工作 , springBoot起到一个整合其他…

LeetCode刷题--- 黄金矿工

个人主页&#xff1a;元清加油_【C】,【C语言】,【数据结构与算法】-CSDN博客 个人专栏 力扣递归算法题 http://t.csdnimg.cn/yUl2I 【C】 ​​​​​​http://t.csdnimg.cn/6AbpV 数据结构与算法 ​​​​http://t.csdnimg.cn/hKh2l 前言&#xff1a;这个专栏主要讲述…

基于SSM的学生信息管理系统

基于SSM的学生信息管理系统资源-CSDN文库 项目介绍 学生管理系统是我从自己学校的综合信息平台得到灵感&#xff0c;于是使用学习过的Spring、SpringMVC、Mybatis框架LayUI完成了这么一套系统。 项目整体难度不大&#xff0c;部署简单&#xff0c;界面友好&#xff0c;代码结…

免费API-JSONPlaceholder使用手册

官方使用指南快速索引>>点这里 快速导览&#xff1a; 什么是JSONPlaceholder?有啥用?如何使用JSONPlaceholder? 关于“增”关于“改”关于“查”关于“删”关于“分页查”关于“根据ID查多个” 尝试自己搭一个&#xff1f;扩展的可能&#xff1f; 什么是JSONPlaceho…

机器学习(一) -- 概述

系列文章目录 机器学习&#xff08;一&#xff09; -- 概述 机器学习&#xff08;二&#xff09; -- 数据预处理 未完待续…… 目录 系列文章目录 前言 一、机器学习定义&#xff08;是什么&#xff09; 二、机器学习的应用&#xff08;能做什么&#xff09; 三、***机器…

ArkUI动画概述

目录 1、按照页面分类 2、按照功能分类 3、显示动画 4、属性动画 动画的原理是在一个时间段内&#xff0c;多次改变UI外观&#xff0c;由于人眼会产生视觉暂留&#xff0c;所以最终看到的就是一个“连续”的动画。UI的一次改变称为一个动画帧&#xff0c;对应一次屏幕刷新&a…

图像分割实战-系列教程2:Unet系列算法(Unet、Unet++、Unet+++、网络架构、损失计算方法)

图像分割实战-系列教程 总目录 语义分割与实例分割概述 Unet系列算法 1、Unet网络 1.1 概述 整体结构&#xff1a;概述就是编码解码过程简单但是很实用&#xff0c;应用广起初是做医学方向&#xff0c;现在也是 虽然用的不是很多&#xff0c;在16年特别火&#xff0c;在医学…

GRNdb:解码不同人类和小鼠条件下的基因调控网络

GRNdb&#xff1a;解码不同人类和小鼠条件下的基因调控网络 摘要introduction数据收集和处理Single-cell and bulk RNA-seq data collection and processing 单细胞和bulk RNA-seq 数据收集和处理Cell cluster identification for scRNA-seq datasets &#xff08;scRNA-seq 数…

在 Linux 中使用 cat 命令

cat 命令用于打印文本文件的文件内容。至少&#xff0c;大多数 Linux 用户都是这么做的&#xff0c;而且没有什么问题。 cat 实际上代表 “连接(concatenate)”&#xff0c;创建它是为了 合并文本文件。但只要有一个参数&#xff0c;它就会打印文件内容。因此&#xff0c;它是用…

vscode中默认shell选择

terminal.integrated.defaultProfile.linux在vs的Preference的Settings里面搜索terminal.integrated.defaultProfile.linux&#xff0c;默认的应该是null&#xff0c;将其修改为bash即可。 linux———/bin/sh、 /bin/bash、 /bin/dash的区别

[设计模式 Go实现] 创建型~抽象工厂模式

抽象工厂模式用于生成产品族的工厂&#xff0c;所生成的对象是有关联的。 如果抽象工厂退化成生成的对象无关联则成为工厂函数模式。 比如本例子中使用RDB和XML存储订单信息&#xff0c;抽象工厂分别能生成相关的主订单信息和订单详情信息。 如果业务逻辑中需要替换使用的时候…

基于JWT的用户token验证

1. 基于session的用户验证 2. 基于token的用户身份验证 3. jwt jwt代码实现方式 1. 导包 <dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.18.2</version> </dependency> 2. 在登录…

golang锁源码【只有关键逻辑】

条件锁 type Cond struct {L Lockernotify notifyList } type notifyList struct {wait uint32 //表示当前 Wait 的最大 ticket 值notify uint32 //表示目前已唤醒的 goroutine 的 ticket 的最大值lock uintptr // key field of the mutexhead unsafe.Pointer //链表头…

Redis经典五大类型源码及底层实现(一)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理、分布式技术原理、数据库技术&#x1f525;如果感觉博主的文章还不错的…

Excel模板填充:从minio上获取模板使用easyExcel填充

最近工作中有个excel导出的功能&#xff0c;要求导出的模板和客户提供的模板一致&#xff0c;而客户提供的模板有着复杂的表头和独特列表风格&#xff0c;像以往使用poi去画是非常耗时间的&#xff0c;比如需要考虑字体大小&#xff0c;单元格合并&#xff0c;单元格的格式等问…

Cisco模拟器-企业网络部署

某企业园区网有&#xff1a;2个分厂&#xff08;分别是&#xff1a;零件分厂、总装分厂&#xff09;1个总厂网络中心 1个总厂会议室&#xff1b; &#xff08;1&#xff09;每个分厂有自己的路由器&#xff0c;均各有&#xff1a;1个楼宇分厂网络中心 每个楼宇均包含&#x…