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)) {// 生成分页的缓存 keyCacheKey pageKey = cacheKey;// 处理参数对象parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);// 调用方言获取分页 sqlString 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 byString 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() {//这个方法即使不分页也会被执行,所以要判断 nullAbstractHelperDialect 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查询,否则会失效!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

探索 3D 图形处理的奥秘

最近一年多来&#xff0c;在 3Dfx、Intel 们的狂轰滥炸中&#xff0c;在 Quake、古墓丽影们的推波助澜下&#xff0c;三维图形已经成为计算机迷眼中的又一个热点。3D 世界到底是怎样的神奇&#xff0c;我们又是怎样享受它的乐趣呢&#xff1f;就让我们来一探究竟吧。 图形基础…

K8s资源管理介绍

用这个官网下的&#xff0c;kube-flannel.yml &#xff0c;就不会nodes not-ready --- kind: Namespace apiVersion: v1 metadata:name: kube-flannellabels:k8s-app: flannelpod-security.kubernetes.io/enforce: privileged --- kind: ClusterRole apiVersion: rbac.author…

递归详解之青蛙跳台阶和汉诺塔问题

&#x1d649;&#x1d65e;&#x1d658;&#x1d65a;!!&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦&#x1f44f;&#x1f3fb;‧✧̣̥̇‧✦ &#x1f44f;&#x1f3fb;‧✧̣̥̇:Solitary-walk ⸝⋆ ━━━┓ - 个性标签 - &#xff1a;来于“云”的“羽球人”。…

51单片机三种编译模式的相互关系

51单片机三种编译模式的相互关系 编译模式默认存储类型RAM使用规模变量使用特点SAMLLdata128B片内RAM使用规模CPU访问数据速度快&#xff0c;但存储容量较小COMPACTpdata258B片外分页RAM速度和容量介于上下两者之间LARGExdata64KB片外RAMCPU访问数据的速度较慢&#xff0c;但存…

idea利用JRebel插件,无需重启,实现Spring Boot项目热重载,节省开发时间和精力!

插件介绍 官方介绍 翻译过来的意思是&#xff1a; JRebel 是一款提高开发效率的工具&#xff0c;允许开发者立即重新加载代码更改。它跳过了在Java开发中常见的重新构建、重启和重新部署循环。JRebel 能够让开发者在相同的时间内完成更多工作&#xff0c;并且在编码时能够保持…

Linux Ubuntu 和 Mac ,Windows 部署离线通义千问1.8B参数 4-bit量化大模型详细教程 全网唯一

代码为我自己写的 python 程序,应该是全网最简单的大模型部署方案,一个模型文件1.3GB,一个python代码即可开始玩耍~ 首先说明,本文下面的内容需要一点点python基础,需要会安装 python和 pip 安装依赖库,如果你不会或者不想这么麻烦,只想要下载就能马上使用的大模型离线整…

【Unity3D】Unity3D技术栈

1 前言 本文梳理了笔者在学习 Unity3D 的过程中&#xff0c;对 Unity3D 的理解和学习路线&#xff0c;以帮助读者循序渐进地学习 Unity3D&#xff0c;后续笔者仍会持续更新 Unity3D 相关技术栈&#xff0c;并同步到本文中。 1.1 专栏文章特点 知识系统&#xff1a;专栏从资源…

python------Pymysql模块

查询 总结&#xff1a; fetchall(): 获取所有查询到的内容。返回结果是嵌套的元组 fetchone(): 获取一条数据。返回结果就是一个非嵌套的元组 fetchmany(num): 返回结果是元组嵌套&#xff0c;底层也是有游标的。如果不传递参数num&#xff0c;默认读取一条数据 注意: 与文件…

CSS与JavaScript的简单认识

CSS&#xff1a;是一门语言&#xff0c;用于控制网页表现&#xff0c;让页面更好看的。 CSS&#xff08;Cascading Style Sheet&#xff09;&#xff1a;层叠样式表 CSS与html结合的三种方式&#xff1a; 1、内部样式&#xff1a;用style标签&#xff0c;在标签内部定义CSS样式…

vim学习笔记

vim学习笔记 Linux Vim编辑器的基本使用 显示行号 set nu 自动补全 CTRL-N或CTRL-P $到当前行的末尾 u 撤销上一步的操作 Ctrlr 恢复上一步被撤销的操作 vim下配置tab缩进格数 原始文件&#xff1a; helloworld nice 普通缩进 shift > &#xff08;或者 Shift <…

springcloud微服务篇--6.网关Gateway

一、为什么需要网关&#xff1f; 网关功能&#xff1a; 身份认证和权限校验 服务路由、负载均衡 请求限流 在SpringCloud中网关的实现包括两种&#xff1a; gateway zuul Zuul是基于Servlet的实现&#xff0c;属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的Web…

【SpringCloud笔记】(12)分布式请求链路跟踪之Sleuth

Sleuth 背景 在微服务框架中&#xff0c;一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果&#xff0c;每一个前段请求都会形成一条复杂的分布式服务调用链路&#xff0c;链路中的任何一环出现高延时或错误都会引起整个请求最后的…

通信原理课设(gec6818) 006:网络编程

目录 1、概念 2、通信 3、通信基本流程 TCP: UDP: 4、函数 I 创建套接字 II 绑定地址 III 字节序转换 IV 地址转换 V 监听 VI accept VII connect VIII 从套接字接收信息 IX 从套接字发送消息 X 关闭套接字 5、网络配置 1、确保你的网卡里面有两个虚拟网卡&a…

WeNet语音识别分词制作词云图

在线体验 ,点击识别语音需要等待一会&#xff0c;文件太大缓存会报错 介绍 本篇博客将介绍如何使用 Streamlit、jieba、wenet 和其他 Python 库&#xff0c;结合语音识别&#xff08;WeNet&#xff09;和词云生成&#xff0c;构建一个功能丰富的应用程序。我们将深入了解代码…

JavaScript 中类和构造函数的区别

JavaScript 中的类和构造函数在实现上有一些区别&#xff0c;但它们的目的相同&#xff0c;都是用于创建对象。 构造函数是一种特殊的函数&#xff0c;它用于创建对象。构造函数使用 new 关键字来创建一个对象实例&#xff0c;并将属性和方法添加到该实例上。构造函数一般以大…

XXL-JOB学习笔记-基于注解实现自动注册新建任务

项目每次注册新建任务都需要手动操作配置xxl-job&#xff0c;不同的环境都需要手动操作配置一次&#xff0c;比较麻烦&#xff0c;为此想要简化相关的手动操作工作&#xff0c;基于注解的形式实现自动注册新建任务。 本篇是在之前一篇基于代码实现新建任务的基础上进一步实现。…

AUTOSAR从入门到精通- 虚拟功能总线(RTE)(三)

目录 前言 RTE的主要功能 几个高频面试题目 RTE基础面试题目 什么是数据转换?

第2课 使用FFmpeg读取rtmp流并用openCV显示视频

本课对应源文件下载链接&#xff1a; https://download.csdn.net/download/XiBuQiuChong/88680079 这节课我们开始利用ffmpeg和opencv来实现一个rtmp播放器。播放器的最基本功能其实就两个:显示画面和播放声音。在实现这两个功能前&#xff0c;我们需要先用ffmpeg连接到rtmp服…

LVS负载均衡配置虚拟引起微服务注册混乱

线上小程序突然报错&#xff0c;查看网关日志&#xff0c;访问下游微服务A时大量报错&#xff1a; 1&#xff09;检查微服务是否未注册。登录eureka页面&#xff0c;发现三个节点均正常注册 三个微服务节点地址分别为&#xff1a;13.9.1.91:8080&#xff0c;13.9.1.92:8080和1…

开源服务指南使用手册

开源服务指南是什么&#xff1f;从哪里来&#xff1f;往哪里去&#xff1f; 定位 用中文推荐优质开源项目&#xff0c;让开发者更容易找到趁手的开源工具。 Slogan 发现开源之美&#xff0c;碰撞无限可能。 我们想要让大家知道更多优秀的开源项目&#xff0c;发现开源的美…