1. 问题现象
在使用 PageHelper 插件开发查询接口时,出现分页失效的情况:接口返回了全部数据而非当前页数据,且 PageInfo 对象中的分页元数据(如 total 总条数、pages 总页数)计算错误。
❌ 错误代码示例
java
public BaseResponse<MyDataDTO> queryDataList(int pageNum, int pageSize, Map<String, Object> params) {
BaseResponse response = new BaseResponse();
// 1. 错误:在设置分页参数前执行了数据库查询
List<MyDataDTO> dataList = myMapper.selectData(params);
// 2. 判空逻辑
if (!CollectionUtils.isEmpty(dataList)) {
// 3. 错误:查询早已完成,此时调用 startPage 无效
PageHelper.startPage(pageNum, pageSize);
// 4. 错误:直接使用全量 List 包装 PageInfo,无法获取数据库真实总数
PageInfo<MyDataDTO> pageInfo = new PageInfo<>(dataList);
response.setResult(wrapPageResult(pageInfo));
return response;
}
response.setResult(Collections.EMPTY_LIST);
return response;
}
2. 原理分析
PageHelper 的核心机制基于 ThreadLocal 和 MyBatis 拦截器(Interceptor)。
2.1 执行流程
PageHelper 不是在内存中对结果集进行截取,而是通过拦截器修改 SQL 语句。
- 设置参数 :调用
PageHelper.startPage(...)时,插件会将分页参数(pageNum, pageSize)存入当前线程的ThreadLocal中。 - 拦截 SQL:当 MyBatis 执行 Mapper 方法时,PageHelper 拦截器会触发。
- SQL 改写 :
- 拦截器检查
ThreadLocal中是否存在分页参数。 - 若存在 :拦截器会根据数据库方言(如 MySQL)生成
SELECT COUNT(0)语句获取总数,并将原 SQL 改写为带LIMIT/OFFSET的分页 SQL 执行。 - 若不存在:拦截器直接放行,执行原始 SQL。
- 拦截器检查
- 清理上下文 :SQL 执行结束后,拦截器会清除
ThreadLocal中的分页参数,避免污染后续查询。
2.2 失效原因
在上述错误代码中:
- 执行顺序错误 :
myMapper.selectData在PageHelper.startPage之前执行。 - 拦截失败 :执行查询时,
ThreadLocal中没有任何分页参数,拦截器未生效,MyBatis 执行了全量查询。 - 参数无效 :查询结束后才调用
startPage,虽然设置了ThreadLocal,但 SQL 交互已结束,该参数未被消费。 - 元数据错误 :
PageInfo接收的是全量 List,因此它只能基于 List 的大小计算total,导致分页信息不符合预期。
3. ✅ 正确实现
核心原则 :PageHelper.startPage 必须紧邻 Mapper 查询方法之前调用。
修正代码
java
public BaseResponse<MyDataDTO> queryDataList(int pageNum, int pageSize, Map<String, Object> params) {
BaseResponse response = new BaseResponse();
// 1. 设置分页参数(存入 ThreadLocal)
PageHelper.startPage(pageNum, pageSize);
// 2. 执行查询(拦截器生效,自动改写 SQL 并执行 Count 查询)
// 注意:此时返回的 list 实际类型为 Page<E>
List<MyDataDTO> dataList = myMapper.selectData(params);
// 3. 获取分页结果
PageInfo<MyDataDTO> pageInfo = new PageInfo<>(dataList);
// 4. 结果处理(PageInfo 可安全处理空集合)
if (!CollectionUtils.isEmpty(dataList)) {
// pageInfo.getTotal() 为数据库真实总数
response.setResult(wrapPageResult(pageInfo));
} else {
response.setResult(Collections.EMPTY_LIST);
}
return response;
}
4. 最佳实践与注意事项
-
严格遵守调用顺序 必须保证
startPage->Mapper查询->PageInfo包装的执行顺序。 -
避免逻辑穿插 严禁在
startPage和Mapper查询之间插入其他 SQL 操作或复杂逻辑。- 风险:PageHelper 的分页参数是"一次性消费"的。如果在分页查询前插入了其他 SQL(如查询用户信息),分页参数会被那条 SQL 消费掉,导致原本需要分页的主查询失效。
-
PageInfo 的健壮性 无需为了判空调整代码顺序。
PageInfo对空 List 有良好的兼容性,若查询结果为空,它会自动设置total=0,不会抛出异常。 -
大数据量风险 如果因顺序错误导致分页失效,全量查询可能会将百万级数据加载至内存,极易引发 OOM(内存溢出),影响系统稳定性。