Github PageHelper 原理解析

任何服务对数据库的日常操作,都离不开增删改查。如果一次查询的纪录很多,那我们必须采用分页的方式。对于一个Springboot项目,访问和查询MySQL数据库,持久化框架可以使用MyBatis,分页工具可以使用github的 PageHelper。我们来看一下PageHelper的使用方法:

// 组装查询条件
ArticleVO articleVO = new ArticleVO();
articleVO.setAuthor("张三");// 初始化返回类
// ResponsePages类是这样一种返回类,其中包括返回代码code和返回消息msg
// 还包括返回的数据和分页信息
// 其中,分页信息就是 com.github.pagehelper.Page<?> 类型
ResponsePages<List<ArticleVO>> responsePages = new ResponsePages<>();// 这里为了简单,写死分页参数。正确的做法是从查询条件中获取
// 假设需要获取第1页的数据,每页20条记录
// com.github.pagehelper.Page<?> 类的基本字段如下
// pageNum: 当前页
// pageSize: 每页条数
// total: 总记录数
// pages: 总页数
com.github.pagehelper.Page<?> page = PageHelper.startPage(1, 20);// 根据条件获取文章列表
List<ArticleVO> articleList = articleMapper.getArticleListByCondition(articleVO);// 设置返回数据
responsePages.setData(articleList);// 设置分页信息
responsePages.setPage(page);

如代码所示,page 是组装好的分页参数,即每页显示20条记录,并且显示第1页。然后我们执行mapper的获取文章列表的方法,返回了结果。此时我们查看 responsePages 的内容,可以看到 articleList 中有20条记录,page中包括当前页,每页条数,总记录数,总页数等信息。

使用方法就是这么简单,但是仅仅知道如何使用还不够,还需要对原理有所了解。下面就来看看,PageHelper 实现分页的原理。

我们先来看看 startPage 方法。进入此方法,发现一堆方法重载,最后进入真正的 startPage 方法,有5个参数,如下所示:

/*** 开始分页** @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 = SqlUtil.getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}SqlUtil.setLocalPage(page);return page;
}

getLocalPage 和 setLocalPage 方法做了什么操作?我们进入基类 BaseSqlUtil 看一下:

package com.github.pagehelper.util;
...public class BaseSqlUtil {// 省略其他代码private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();/*** 从 ThreadLocal<Page> 中获取 page*/public static <T> Page<T> getLocalPage() {return LOCAL_PAGE.get();}/*** 将 page 设置到 ThreadLocal<Page>*/public static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}// 省略其他代码
}

原来是将 page 放入了 ThreadLocal 中。ThreadLocal 是每个线程独有的变量,与其他线程不影响,是放置 page 的好地方。

setLocalPage 之后,一定有地方 getLocalPage,我们跟踪进入代码来看。

有了MyBatis动态代理的知识后,我们知道最终执行SQL的地方是 MapperMethod 的 execute 方法,作为回顾,我们来看一下:

package org.apache.ibatis.binding;
...public class MapperMethod {public Object execute(SqlSession sqlSession, Object[] args) {Object result;if (SqlCommandType.INSERT == command.getType()) {// 省略} else if (SqlCommandType.UPDATE == command.getType()) {// 省略} else if (SqlCommandType.DELETE == command.getType()) {// 省略} else if (SqlCommandType.SELECT == command.getType()) {if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {/*** 获取多条记录*/result = executeForMany(sqlSession, args);} else if ...// 省略} else if (SqlCommandType.FLUSH == command.getType()) {// 省略} else {throw new BindingException("Unknown execution method for: " + command.getName());}...return result;}
}

由于执行的是select操作,并且需要查询多条纪录,所以我们进入 executeForMany 这个方法中,然后进入 selectList 方法,然后是 executor.query 方法。再然后突然进入到了 mybatis 的 Plugin 类的 invoke 方法,这是为什么?

这里就必须提到 mybatis 提供的 Interceptor 接口。**Intercept 机制让我们可以将自己制作的分页插件 intercept 到查询语句执行的地方,这是MyBatis对外提供的标准接口。**借助于Java的动态代理,标准的拦截器可以拦截在指定的数据库访问流程中,执行拦截器自定义的逻辑,比如在执行SQL之前拦截,拼装一个分页的SQL并执行。

让我们回到MyBatis初始化的时候,我们发现 MyBatis 为我们组装了 sqlSessionFactory,所有的 sqlSession 都是生成自这个 Factory。在这篇文章中,我们将重点放在 interceptorChain 上。程序启动时,MyBatis 或者是 mybatis-spring 会扫描代码中所有实现了 interceptor 接口的插件,并将它们以【拦截器集合】的方式,存储在 interceptorChain 中。如下所示:

# sqlSessionFactory 中的重要信息sqlSessionFactoryconfigurationenvironment        mapperRegistryconfig         knownMappers   mappedStatements   resultMaps         sqlFragments       interceptorChain   # MyBatis拦截器调用链interceptors   # 拦截器集合,记录了所有实现了Interceptor接口,并且使用了invocation变量的类

如果MyBatis检测到有拦截器,它就会在拦截器指定的执行点,首先执行 Plugin 的 invoke 方法,唤醒拦截器,然后执行拦截器定义的逻辑。因此,当 query 方法即将执行的时候,其实执行的是拦截器的逻辑。

MyBatis官网的说明:

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

如果想了解更多拦截器的知识,可以看文末的参考资料。

我们回到主线,继续看Plugin类的invoke方法:

package org.apache.ibatis.plugin;
...public class Plugin implements InvocationHandler {...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 方法的实现,这里我们进入【PageHelper】类来看:

package com.github.pagehelper;
.../*** Mybatis - 通用分页拦截器*/
@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper extends BasePageHelper implements Interceptor {private final SqlUtil sqlUtil = new SqlUtil();@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 执行 sqlUtil 的拦截逻辑return sqlUtil.intercept(invocation);}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {sqlUtil.setProperties(properties);}
}

可以看到最终调用了 SqlUtil 的intercept 方法,里面的 doIntercept 方法是 PageHelper 原理中最重要的方法。跟进来看:

package com.github.pagehelper.util;
...public class SqlUtil extends BaseSqlUtil implements Constant {.../*** 真正的拦截器方法** @param invocation* @return* @throws Throwable*/public Object intercept(Invocation invocation) throws Throwable {try {return doIntercept(invocation);  // 执行拦截} finally {clearLocalPage();  // 清空 ThreadLocal<Page>}}/*** 真正的拦截器方法** @param invocation* @return* @throws Throwable*/public Object doIntercept(Invocation invocation) throws Throwable {// 省略其他代码// 调用方法判断是否需要进行分页if (!runtimeDialect.skip(ms, parameterObject, rowBounds)) {ResultHandler resultHandler = (ResultHandler) args[3];// 当前的目标对象Executor executor = (Executor) invocation.getTarget();/*** getBoundSql 方法执行后,boundSql 中保存的是没有 limit 的sql语句*/BoundSql boundSql = ms.getBoundSql(parameterObject);// 反射获取动态参数Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);// 判断是否需要进行 count 查询,默认需要if (runtimeDialect.beforeCount(ms, parameterObject, rowBounds)) {// 省略代码// 执行 count 查询Object countResultList = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);Long count = (Long) ((List) countResultList).get(0);// 处理查询总数,从 ThreadLocal<Page> 中取出 page 并设置 totalruntimeDialect.afterCount(count, parameterObject, rowBounds);if (count == 0L) {// 当查询总数为 0 时,直接返回空的结果return runtimeDialect.afterPage(new ArrayList(), parameterObject, rowBounds);}}// 判断是否需要进行分页查询if (runtimeDialect.beforePage(ms, parameterObject, rowBounds)) {/*** 生成分页的缓存 key* pageKey变量是分页参数存放的地方*/CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);/*** 处理参数对象,会从 ThreadLocal<Page> 中将分页参数取出来,放入 pageKey 中* 主要逻辑就是这样,代码就不再单独贴出来了,有兴趣的同学可以跟进验证*/parameterObject = runtimeDialect.processParameterObject(ms, parameterObject, boundSql, pageKey);/*** 调用方言获取分页 sql* 该方法执行后,pageSql中保存的sql语句,被加上了 limit 语句*/String pageSql = runtimeDialect.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);//设置动态参数for (String key : additionalParameters.keySet()) {pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));}/*** 执行分页查询*/resultList = executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);} else {resultList = new ArrayList();}} else {args[2] = RowBounds.DEFAULT;// 不需要分页查询,执行原方法,不走代理resultList = (List) invocation.proceed();}/*** 主要逻辑:* 从 ThreadLocal<Page> 中取出 page* 将 resultList 塞进 page,并返回*/return runtimeDialect.afterPage(resultList, parameterObject, rowBounds);}...
}

Count 查询语句 countBoundSql 被执行了,分页查询语句 pageBoundSql 也被执行了。然后从 ThreadLocal 中将page 取出来,设置记录总数,每页条数等信息,同时也将查询到的记录塞进page,最后返回。再之后就是mybatis的常规后续操作了。

知识拓展

我们来看看 PageHelper 支持哪些数据库的分页操作:

  1. Oracle
  2. Mysql
  3. MariaDB
  4. SQLite
  5. Hsqldb
  6. PostgreSQL
  7. DB2
  8. SqlServer(2005,2008)
  9. Informix
  10. H2
  11. SqlServer2012
  12. Derby
  13. Phoenix

原来 PageHelper 支持这么多数据库,那么持久化工具mybatis为什么不一口气把分页也做了呢?

其实mybatis也有自带的分页方法: RowBounds。RowBounds简单地来说包括 offset 和 limit。实现原理是将所有符合条件的记录获取出来,然后丢弃 offset 之前的数据,只获取 limit 条数据。这种做法效率低下,个人猜想mybatis只想把数据库连接和SQL执行这方面做精做强,至于如分页之类的细节,本身提供Intercept接口,让第三方实现该接口来完成分页。PageHelper 就是这样的第三方分页插件。甚至你可以实现该接口,制作你自己的业务逻辑,拦截到任何MyBatis允许你拦截的地方。

总结

PageHelper 的分页原理,最核心的部分是实现了 MyBatis 的 Interceptor 接口,从而将分页参数拦截在执行sql之前,拼装出分页sql到数据库中执行。

初始化的时候,因为 PageHelper 的 SqlUtil 中实例化了 intercept 方法,因此MyBatis 将它视作一个拦截器,记录在 interceptorChain 中。

执行的时候,PageHelper首先将 page 需求记录在 ThreadLocal< Page> 中,然后在拦截的时候,从 ThreadLocal< Page> 中取出 page,拼装出分页sql,然后执行。

同时将结果分页信息(包括当前页,每页条数,总页数,总记录数等)设置回page,让业务代码可以获取。

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

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

相关文章

Springboot Mybatis使用PageHelper实现分页查询

以下介绍实战中数据库框架使用的是mybatis&#xff0c;对整合mybatis此处不做介绍。 使用pageHelper实现分页查询其实非常简单&#xff0c;共两步&#xff1a; 一、导入依赖&#xff1b; pom.xml添加依赖&#xff1a; <!--分页插件 pagehelper --> <dependency>…

mysql 与 xls 连接_数据库MySQL与xls文件的互导

最近的一个项目需要将xls表导入到MySQL数据库中和将MySQL数据表导出到xls表中&#xff0c;在网上搜了很多资料&#xff0c;经过多次尝试终于实现了功能&#xff0c;废话不多说&#xff0c;在这粘贴出代码&#xff0c;希望可以帮到需要的朋友。一、将.xls表导入到MySQL数据表中。…

Mysql 一条SQL语句实现批量更新数据,update结合case、when和then的使用案例

如何用一条sql语句实现批量更新&#xff1f;mysql并没有提供直接的方法来实现批量更新&#xff0c;但是可以用点小技巧来实现。 代码如下: UPDATE mytable SET myfield CASE id WHEN 1 THEN valueWHEN 2 THEN valueWHEN 3 THEN value END WHERE id IN (1,2,3);这里使用了c…

MySQL可运行在不同的操作系统下_不同操作系统下的mysql数据库同步

当数据库的数据量读写频繁的时候&#xff0c;我们就要考虑把数据库的读写分开&#xff0c;以提高数据库的使用效率&#xff0c;(当然还有其他用处比如&#xff0c;备份数据)&#xff0c;这个时候我们就要用到mysql的一个功能&#xff0c;数据库同步。下面就具体讲一下实现方法。…

@RequestBody和@RequestParam区别

一&#xff1a;RequestBody和RequestParam区别 RequestParam 用来处理Content-Type: 为 application/x-www-form-urlencoded编码的内容。&#xff08;Http协议中&#xff0c;如果不指定Content-Type&#xff0c;则默认传递的参数就是application/x-www-form-urlencoded类型&am…

mysql内置加密函数_MySQL数据库内置加密函数总结

首先&#xff0c;我认识的加密函数有以下几个&#xff1a;password(plainText)&#xff1a;旧版(OLD_PASSWORD())加密后长度16位&#xff0c;新版41位select length(password("123456"))可以用来查看加密后的字符串的长度。这种加密方法依赖数据库&#xff0c;需要保…

Query String Parameters、Form Data、Request Payload的区别

Query String Parameters 当发起一次GET请求时&#xff0c;参数会以url string的形式进行传递。即?后的字符串则为其请求参数&#xff0c;并以&作为分隔符。 如下http请求报文头&#xff1a; GeneralRequest URL: http://login?unamedingdingRequest Method: GETQuery…

c mysql 添加数据类型_MYSQL的常用命令和增删改查语句和数据类型

连接命令&#xff1a;mysql -h[主机地址] -u[用户名] -p[用户密码]创建数据库&#xff1a;create database [库名]显示所有数据库: show databases;打开数据库:use [库名]当前选择的库状态:SELECT DATABASE();创建数据表:CREATE TABLE [表名]([字段名] [字段类型]([字段要求]) …

@requestbody和@requestparam作用

1、什么都不写 GET 可以自动封装为对象模型&#xff0c;没有的数值自动为0值 POST 请求体里面放了数据&#xff0c;但是还是使用了RequestParam里的数据 总结&#xff1a; 在不使用注解的情况下&#xff0c;相当于默认使用了RequestParam里的数据 &#xff08;这种理解是错…

@RequestBody的使用

提示&#xff1a;建议一定要看后面的RequestBody的核心逻辑源码以及六个重要结论本文前半部分的内容都是一些基 本知识常识&#xff0c;可选择性跳过。 声明&#xff1a;本文是基于SpringBoot&#xff0c;进行的演示说明。 基础知识介绍&#xff1a; ​ RequestBody主要用来接…

mysql 字段值为 a b c_数据的虚拟列-让数据库中的C字段等于a+b

直接用update aaaaa aa set aa.f (aa.aaa.b)就行了引出来的新知识--虚拟列虚拟列的好处是:如果cab那插入数据时&#xff1b;值只插入a1,b2;那么c会自动是3oracle temporary table and virtual column(2013-07-31 15:06:55)表是数据库中保存用户数据最基本的结构。下面我简要的…

@RequestBody 和 @RequestParam可以同时使用

RequestParam和RequestBody这两个注解是可以同时使用的。 网上有很多博客说RequestParam 和RequestBody不能同时使用&#xff0c;这是错误的。根据HTTP协议&#xff0c;并没有说post请求不能带URL参数&#xff0c;经验证往一个带有参数的URL发送post请求也是可以成功的。只不过…

mysql索引使增删变慢_mysql优化之索引篇

mysql&#xff0c;对it打工人&#xff0c;这个几乎是必备的技能之一。mysql可以解决我们平时工作中的大量的、有关增删查改的问题。所以想深入了解mysql&#xff0c;我觉得关键在于他的增删查改背后的算法&#xff0c;开搞。面对增删查改等问题&#xff0c;直接通过场景来看吧场…

POST、GET、@RequestBody和@RequestParam区别

RequestParam 注解RequestParam接收的参数是来自HTTP请求体或请求url的QueryString中。 RequestParam可以接受简单类型的属性&#xff0c;也可以接受对象类型。 RequestParam有三个配置参数&#xff1a; required 表示是否必须&#xff0c;默认为 true&#xff0c;必须。de…

python与linux关系_如何处理Linux / Python依赖关系?

由于缺乏对我想使用的一些库的支持,我将一些Python开发从Windows转移到Linux开发.我已经花了大部分时间搞乱了依赖关系.问题每当我拿起Linux,我通常会遇到一些依赖问题,通常是开发库,无论是通过apt-get,easy_install或pip安装.我可以浪费时间,应该是简单的任务,花更长的时间让图…

Springmvc接收json数据的4种方式

1、以RequestParam接收 前端传来的是json数据不多时&#xff1a;[id:id],可以直接用RequestParam来获取值 Autowired private AccomodationService accomodationService;RequestMapping(value "/update") ResponseBody public String updateAttr(RequestParam (&q…

centos安装mysql卡住_CentOS 6.4安装MySQL的过程中出现的bug

在CentOS 6.4下安装MySQL&#xff0c;下载一个源码包&#xff0c;感觉很简单&#xff0c;tar&#xff0c;./configure&#xff0c;make&#xff0c;make install 经典四步&#xff0c;弄这个也不是第一次了&#xff0c;但今天就是老出错&#xff0c;首先是./configure中&#x…

SpringMVC接受List的几种方式

说到SpringMVC接受List参数&#xff0c;可能有人会说这个是个坑&#xff01;下面我来跟大家说怎么补这个坑。下面我分几种情况来讲&#xff1a; 一、只接收一个List 1、form表单提交 controller代码&#xff1a; ResponseBody RequestMapping("test") public voi…

mysql怎么加固_mysql安装及加固

mysql安装查看是否安装mysql我们先看一下有没有安装mysqlyum list installed mysql | grep mysql本地只安装了php链接mysql的库&#xff0c;没有安装mysql如果有安装&#xff0c;可以使用安装的或者卸载了重新安装添加yum源去mysql官网查找下面我就来安装下yum源sudo rpm -ivh …

python标准库os的方法listdir_使用python标准库快速修改文件名字

大家在追剧的时候会一次性下载很多电影&#xff0c;但是很烦人的是前面会有很多电影网站的广告前缀。今天我将介绍一个简短的代码&#xff0c;快速修改这些文件的名字。工具&#xff1a;os首先在电影目录下新建一个py文件&#xff0c;并导入osimport osos&#xff0c;是python中…