PageHelper的使用及原理、PageHelper分页查询源码分析

PageHelper的使用及原理、PageHelper分页查询源码分析

文章目录

一、PageHelper的介绍二、PageHelper的使用2.1、引入依赖2.2、配置PageHelper(非必须)2.3、使用

三、PageHelper的原理3.1、PageHelper的分页原理3.2、PageHelper-Spring-Boot-Starter的集成原理

四、源码分析4.1、使用 ThreadLocal 记录分页参数4.2、拦截器改造 SQL4.2.1、统计总数4.2.2、分页查询

4.3、new PageInfo<>(result)

五、其他问题5.1、关闭总数查询

一、PageHelper的介绍

PageHelper是Mybatis-Plus中的一个插件,主要用于实现数据库的分页查询功能。其核心原理是将传入的页码和条数赋值给一个Page对象,并保存到本地线程ThreadLocal中。接下来,PageHelper会进入Mybatis的拦截器环节,在拦截器中获取并处理刚才保存在ThreadLocal中的分页参数。这些分页参数会与原本的SQL语句和内部已经定义好的SQL进行拼接,从而完成带有分页处理的SQL语句的构建。

PageHelper 是国内非常优秀的一款开源 mybatis 分页插件,它支持常用的主流数据库,例如 Oracle、Mysql、MariaDB、SQLite、Hsqldb 等。

二、PageHelper的使用

2.1、引入依赖

pom中引入依赖

com.github.pagehelper

pagehelper-spring-boot-starter

1.4.0

2.2、配置PageHelper(非必须)

该步骤是非必须的,若不配置则是默认的,对正常使用影响不大。

在application.properties或application.yml中配置pagehelper相关属性,例如:

以application.yml为例:

pagehelper:

helperDialect: mysql #指定数据库方言,这里以MySOL为例

reasonable: true #分页合理化,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页

supportMethodsArguments: true #支持通过Mapper接口参数传递分页参数

params: count=countSql #用于从对象中根据属性名取值,这里配置count的SOL

这些参数的详细解释如下:

helperDialect: 指定分页插件的数据库方言。PageHelper会自动检测当前的数据库链接,自动选择合适的分页方式。如果你使用的是MySQL,可以明确指定为mysql。reasonable: 是否启用分页合理化。如果启用,当pageNum<1时,会自动查询第一页的数据,当pageNum>pages时,自动查询最后一页数据;不启用的情况下,以上两种情况都会返回空数据。supportMethodsArguments: 是否支持通过Mapper接口参数来传递分页参数,默认值false。设置为true时,PageHelper会从查询方法的参数值中自动根据配置的字段取值,进行分页。 (具体使用见下方源码讲解)params: 用于从对象中根据属性名取值,可以配置pageNum, pageSize, count, pageSizeZero, reasonable等参数。这里的count=countSql表示在执行分页查询时,会使用countSql作为计算总数的SQL。

2.3、使用

PageHelper 的使用也非常简单,只需要在查询之前调用PageHelper.startPage() 方法即可开始分页。例如:

import com.github.pagehelper.PageHelper;

import com.github.pagehelper.PageInfo;

@Service

public class UserService {

@Autowired

private UserMapper userMapper;

public PageInfo listUser(UserQo qo) {

// 设置分页参数

PageHelper.startPage(qo.getPageNo(),qo.getPageSize());

//查询数据

List result = userMapper.listByCondition(qo);

// 使用PageInfo对查询结果进行封装,返回包含分页信息的对象

return new PageInfo<>(result);

}

}

UserMapper.xml文件

PageHelper 的核心方法是 PageHelper.startPage(),它的作用是为当前线程开启分页上下文,并在接下来的查询中拦截 SQL,添加分页参数。 执行流程:

PageHelper.startPage() 开启分页上下文,并设置分页参数。查询方法 userMapper.listByConfition(qo) 被拦截,PageHelper 在 SQL 后自动添加 LIMIT。查询返回结果后,使用 PageInfo 封装结果,同时计算总记录数、分页信息等。

查询日志: 如上图:开启分页查询后会自动查询count总数。

三、PageHelper的原理

3.1、PageHelper的分页原理

拦截器机制: PageHelper通过MyBatis的拦截器机制实现分页功能。它会在SQL执行前拦截并修改SQL语句,添加分页相关的信息。

ThreadLocal存储分页参数: 在调用分页查询之前,会将分页参数(如页码、每页数量)存储在当前线程的ThreadLocal中,确保每次查询都能获取到正确的分页信息。

自动构建分页SQL: 根据存储在ThreadLocal中的分页参数,在拦截器中自动构建带有分页逻辑的SQL语句,例如使用LIMIT和OFFSET来限制返回结果集。

执行分页查询: 当执行带有分页参数的查询时,PageHelper会拦截该查询并根据分页参数重新构建SQL语句,然后执行查询操作。

封装分页结果: 在查询完成后,PageHelper会将查询结果封装成包含分页信息的对象,方便在业务逻辑中使用。

3.2、PageHelper-Spring-Boot-Starter的集成原理

自动配置: pagehelper-spring-boot-starter提供了自动配置类,可以根据配置文件中的属性自动配置PageHelper的相关参数,简化了在Spring Boot项目中集成PageHelper的步骤。

注入拦截器: 在自动配置过程中,会向MyBatis的SqlSessionFactory中注入PageInterceptor,这个拦截器负责拦截SQL并处理分页逻辑。

配置参数: 通过在application.properties或application.yml中配置pagehelper相关属性,可以定制化地设置分页参数,如页码参数名、每页数量等。

总的来说,PageHelper通过拦截器机制、ThreadLocal存储分页参数以及自动构建分页SQL来实现对MyBatis的分页支持,而pagehelper-spring-boot-starter则在Spring Boot中简化了PageHelper的集成和配置过程。

四、源码分析

主要弄懂以下问题:

PageHelper.startPage()方法做了什么事。源码里面是在哪个地方判断需要添加count方法的。源码是在哪个地方拼接limit语句的没有调用PageHelper.startPage()方法时为什么某些情况下也能调用分页查询?

以下源码基于pagehelper-5.3.0进行讲解。示例如下:

public PageInfo listUser(UserQo qo) {

// 开始分页

PageHelper.startPage(qo.getPageNo(),qo.getPageSize());

//查询数据

List result = userMapper.listByCondition(qo);

// 封装分页对象

return new PageInfo<>(result);

}

4.1、使用 ThreadLocal 记录分页参数

在调用 PageHelper.startPage() 方法时,会通过 ThreadLocal 存储当前分页参数:

public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {

Page page = new Page(pageNum, pageSize, count);

page.setReasonable(reasonable);

page.setPageSizeZero(pageSizeZero);

//当已经执行过orderBy的时候

Page oldPage = getLocalPage();

if (oldPage != null && oldPage.isOrderByOnly()) {

page.setOrderBy(oldPage.getOrderBy());

}

//设置ThreadLocal 将分页参数设置到ThreadLocal中

setLocalPage(page);

return page;

}

其实主要就是把分页参数给到 Page ,然后将实例 Page 存储到 ThreadLocal 中。

4.2、拦截器改造 SQL

PageHelper 是通过拦截器底层执行 sql,对应的拦截器是 PageInterceptor,首先来看看这个类头部的定义,可以看出拦截了 Executor 的 query方法,毕竟 Mybatis 底层查询实际是借助 SqlSeesion 调用 Executor#query。

注解 @Intercepts:定义了一个拦截器,用于拦截MyBatis的Executor类中的query方法。注解 @Signature:指定了要拦截的方法签名,包括方法类型、方法名和参数类型。可以看出这里拦截了Executor 中6参数query查询, 4参数query 查询PageInterceptor中的intercept方法是整个分页查询的关键,核心代码如下:

单步跟进 dialect.skip 方法。判断是否需要分页查询。

dialect.skip方法是调用了PageHelper.skip的方法,在这个方法里面,重要的代码是pageParams.getPage()这个方法,并且在这个方法中存在一个坑。

Q:从上面PageHelper的使用中我们知道,若要自动分页,则需要在查询方法前面加上PageHelper.startPage(pageNum,pageSize);这行代码。但是实际使用中发现,有时候没有添加这句话,但是也自动分页了,这跟我们的预期不一致,这是为什么呢? A:重点就在于skip方法中的这行代码Page page = pageParams.getPage(parameterObject, rowBounds); 。在这行代码中主要有两个业务逻辑:

若调用了PageHelper.startPage()方法,则将之前存储在ThreadLocal中的Page对象返回若ThreadLocal中没有找到Page对象,则查看请求参数中是否同时存在pageNum、pageSize变量,若同时存在,则将这两个参数转换为Page对象返回。

解决方法: 修改请求参数的变量名,只要pageNum与pageSize不同时出现即可。或者若不需要分页查询,则代码List result = userMapper.listByConfition(qo); qo中的pageNum或pageSize任一参数置为null即可。

那么我们看一下pageParams.getPage()这个方法。 我们在上面PageHelper的使用中讲了application.yml配置文件需要配置pagehelper的属性,若配置了supportMethodsArguments=true, 那么就可能走到下面的逻辑中。若同时存在pageNum与pageSize属性并且两者都不为空,则返回Page对象。

从上面的代码我们知道 dialect.skip 方法会获取到Page对象,所以这里代码会继续往下走。

@Override

public Object intercept(Invocation invocation) throws Throwable {

try {

...... //此处省略

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 dialect.afterPage(resultList, parameter, rowBounds);

} finally {

if(dialect != null){

dialect.afterAll();

}

}

}

4.2.1、统计总数

1.判断是否需要进行 count 查询dialect.beforeCount(ms, parameter, rowBounds) 层层调用后最后会发现返回了true。

在设置分页参数时,两种构造器,设置count是否开启查询总数

//默认开启

this.count = true;

PageMethod.startPage(req.getPageNum(), req.getPageSize());

// 关闭查询总数

PageMethod.startPage(req.getPageNum(), req.getPageSize(),false);

2.查询总数Long count = count(executor, ms, parameter, rowBounds, null, boundSql); 执行自动创建的 count 查询 获取查询总数的sql 将查询的sql包起来,拼装count(sql) ,查询总数。

如果count为0,则直接返回空列表,不进行分页:

4.2.2、分页查询

执行查询resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); sql拼接limit条件 获取limit,拼接到查询sql尾部 分页查询后,处理分页结果集return dialect.afterPage(resultList, parameter, rowBounds);

将查询到的数据集合集存放到Page集合中 Page属于List集合

Page类定义

public class Page extends ArrayList implements Closeable {

}

Page与List关系图

4.3、new PageInfo<>(result)

先在PageInfo父类中执行supep(this)计算数据集的总数,再设置分页参数

五、其他问题

5.1、关闭总数查询

若我们在查询时不想查询总数了该怎么办呢?

不开启总数查询 PageMethod.startPage(qo.getPageNum(), qo.getPageSize(),false); 开启总数查询 PageMethod.startPage(qo.getPageNum(), qo.getPageSize());

创作不易,欢迎打赏,你的鼓励将是我创作的最大动力。

相关推荐

tim聊天软件安全吗(tim这个软件怎么样)
365bet官网网投

tim聊天软件安全吗(tim这个软件怎么样)

📅 08-11 👁️ 212
哪些网贷app额度高更好 ?
bst365老牌体育

哪些网贷app额度高更好 ?

📅 09-14 👁️ 1707
社群时代如何正确的向别人请教问题
bst365老牌体育

社群时代如何正确的向别人请教问题

📅 06-30 👁️ 2461