一、引言
在使用 MyBatis 进行数据访问时,分页查询是最常见的需求之一。PageHelper 是一个基于 MyBatis 的分页插件,通过拦截 Executor 执行的 SQL,自动在原始 SQL 上拼接分页语句,从而实现的分页功能。本文将从功能用法、核心原理以及源码剖析、实际运用几个方面,介绍 PageHelper
二、主要功能与用法
2.1 引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.4.0</version>
</dependency>
2.2 简单的分页
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
// 返回给前端:pageInfo.getList(), pageInfo.getTotal(), pageInfo.getPages()...
pageNum
:当前页码,从 1 开始pageSize
:每页记录数-
返回结果:通过
PageInfo
包装,包含列表、总记录数、总页数、当前页码等
2.3 排序
PageHelper.startPage(pageNum, pageSize)
.setOrderBy("create_time desc, id asc");
List<Order> orders = orderMapper.selectByUserId(userId);
这里调用 setOrderBy可以
支持多字段、多方向排序
2.4 总数的查询
PageHelper.startPage(pageNum, pageSize, false);
List<Product> products = productMapper.selectByCategory(catId);
PageInfo<Product> pageInfo = new PageInfo<>(products);
startPage的第三个参数 表示是否执行 SELECT COUNT(*)
。设置为 false
时,PageHelper 只拼接分页语句,不执行总数查询。
2.5 物理分页与逻辑分页
物理分页指在数据库层面直接实现分页操作,通过修改SQL语句,仅查询当前页所需的数据。这种方式通过数据库自身机制限制返回的数据量,减少网络传输和应用内存消耗,适合大数据量场景。
逻辑分页意思则是:逻辑上(前端上、表面上)实现了分页,实际还是执行了整个SQL语句
坑:不能混用 RowBounds
和 startPage!
(一)RowBounds
(MyBatis原生分页)和 startPage
(PageHelper物理分页)底层均依赖MyBatis的插件拦截机制,混用时可能多次修改SQL,导致分页参数重复添加(如LIMIT
被多次拼接),最终查询结果异常。
(二)startPage
优先级高于RowBounds
。若同时调用:
startPage
会触发物理分页,生成LIMIT
语句。RowBounds
会尝试在内存中二次分页,但此时SQL已限制数据量,可能导致分页结果错误。
物理分页的正确姿势:
//-------1.startPage(推荐)-------------
// 物理分页:查询第2页,每页10条
PageHelper.startPage(2, 10);
List<User> users = userMapper.selectAll();
//----------2.RowBounds----------------
//配置支持
pagehelper.support-methods-arguments=true
pagehelper.params=pageNum=pageNumKey;pageSize=pageSizeKey
//Mapper接口
List<User> selectAllWithRowBounds(RowBounds rowBounds);
// 物理分页:offset=10, limit=10(对应第2页)
List<User> users = userMapper.selectAllWithRowBounds(new RowBounds(10, 10));
example中都实现了:SELECT * FROM users LIMIT 10,10
三、源码剖析
PageHelper 核心在于一个 MyBatis 拦截器 PageInterceptor
,注册为插件后拦截 Executor.query
方法:
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取方法参数:MappedStatement、参数对象、RowBounds、ResultHandler
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
// 2. 判断是否需要分页:检查 ThreadLocal 中是否有分页上下文
Page<?> page = SqlUtil.getLocalPage();
if (page != null) {
// 3. 清理 ThreadLocal,避免后续影响
SqlUtil.removeLocalPage();
// 4. 执行 count 查询(可选)
if (page.isCount()) {
long total = count(ms, parameter);
page.setTotal(total);
}
// 5. 拼接分页 SQL:在原始 SQL 上添加 LIMIT/OFFSET
BoundSql boundSql = ms.getBoundSql(parameter);
String pageSql = dialect.getPageSql(boundSql.getSql(), page.getStartRow(), page.getPageSize());
// 6. 创建新的 MappedStatement,使用 pageSql
MappedStatement pageMs = SqlUtil.newMappedStatement(ms, boundSql, pageSql);
args[0] = pageMs;
args[2] = RowBounds.DEFAULT;
}
// 7. 执行查询
return invocation.proceed();
}
}
-
ThreadLocal 存储:
PageHelper.startPage
会将分页参数封装到Page
对象并放入SqlUtil
的localPage
中。 -
SQL 重写:通过数据库方言
Dialect
(如MysqlDialect
)生成带LIMIT
的分页 SQL。 -
MappedStatement 克隆:在原有
MappedStatement
基础上,构造一个新的包含分页 SQL 的实例。 -
总数查询:执行一次
SELECT COUNT(*)
,通过MappedStatement
的SqlSource
修改为 count 语句。
四、pageHelper配置
pagehelper:
helperDialect: mysql # 数据库方言,可选 mysql, postgresql, oracle...
reasonable: true # 页码 < 1 时,自动查询第一页;页码 > 最大页时,查询最后一页
supportMethodsArguments: false # 支持从 Mapper 方法参数中读取分页参数
params: count=countSql # 自定义 count 查询的参数名