文章目录
一、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中引入依赖
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
// 设置分页参数
PageHelper.startPage(qo.getPageNo(),qo.getPageSize());
//查询数据
List
// 使用PageInfo对查询结果进行封装,返回包含分页信息的对象
return new PageInfo<>(result);
}
}
UserMapper.xml文件
select * from user
where 1=1
name = #{name}
order by id
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
// 开始分页
PageHelper.startPage(qo.getPageNo(),qo.getPageSize());
//查询数据
List
// 封装分页对象
return new PageInfo<>(result);
}
4.1、使用 ThreadLocal 记录分页参数
在调用 PageHelper.startPage() 方法时,会通过 ThreadLocal 存储当前分页参数:
public static
Page
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page
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
那么我们看一下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
}
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());
创作不易,欢迎打赏,你的鼓励将是我创作的最大动力。