PageHelper 分页失效原因分析与正确实践

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 语句。

  1. 设置参数 :调用 PageHelper.startPage(...) 时,插件会将分页参数(pageNum, pageSize)存入当前线程的 ThreadLocal 中。
  2. 拦截 SQL:当 MyBatis 执行 Mapper 方法时,PageHelper 拦截器会触发。
  3. SQL 改写 :
    • 拦截器检查 ThreadLocal 中是否存在分页参数。
    • 若存在 :拦截器会根据数据库方言(如 MySQL)生成 SELECT COUNT(0) 语句获取总数,并将原 SQL 改写为带 LIMIT/OFFSET 的分页 SQL 执行。
    • 若不存在:拦截器直接放行,执行原始 SQL。
  4. 清理上下文 :SQL 执行结束后,拦截器会清除 ThreadLocal 中的分页参数,避免污染后续查询。

2.2 失效原因

在上述错误代码中:

  1. 执行顺序错误 :myMapper.selectData 在 PageHelper.startPage 之前执行。
  2. 拦截失败 :执行查询时,ThreadLocal 中没有任何分页参数,拦截器未生效,MyBatis 执行了全量查询。
  3. 参数无效 :查询结束后才调用 startPage,虽然设置了 ThreadLocal,但 SQL 交互已结束,该参数未被消费。
  4. 元数据错误 :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. 最佳实践与注意事项

  1. 严格遵守调用顺序 必须保证 startPage -> Mapper查询 -> PageInfo包装 的执行顺序。

  2. 避免逻辑穿插 严禁在 startPage 和 Mapper查询 之间插入其他 SQL 操作或复杂逻辑。

    • 风险:PageHelper 的分页参数是"一次性消费"的。如果在分页查询前插入了其他 SQL(如查询用户信息),分页参数会被那条 SQL 消费掉,导致原本需要分页的主查询失效。
  3. PageInfo 的健壮性 无需为了判空调整代码顺序。PageInfo 对空 List 有良好的兼容性,若查询结果为空,它会自动设置 total=0,不会抛出异常。

  4. 大数据量风险 如果因顺序错误导致分页失效,全量查询可能会将百万级数据加载至内存,极易引发 OOM(内存溢出),影响系统稳定性。

相关推荐
小码哥_常14 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
学长毕设17 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
努力的小雨19 小时前
给 AI 眼镜开发应用有多难?我从零做了一个陪练,还上架了
后端
excel21 小时前
双 Token 与前端使用 Crypto 公私钥鉴权换 Token,哪种方案更好?
前端·后端
AI入职记21 小时前
AI 代码审查召回率实测:20 个骗过单元测试的变异体,抓到 13 个
后端
浮链序21 小时前
别让 AI 客服把用户当猴耍:百来行 Python 把「转人工」做成一条高可用链路
后端
属于自己的天空21 小时前
Claude Code 插件完全指南:官方市场怎么用,怎么自己写(v2.1.269+)
后端
Tokenfree21 小时前
给大模型调用加一个熔断器:60 行代码,三个阈值
后端
Jason_chen1 天前
Linux 进程调度器深入解析与实时性优化指南(基于 Linux 5.10+)
后端
136096757231 天前
三个 Agent 干同一个活,职责边界到底该怎么划
后端