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 的核心机制基于 ThreadLocalMyBatis 拦截器(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.selectDataPageHelper.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. 避免逻辑穿插 严禁在 startPageMapper查询 之间插入其他 SQL 操作或复杂逻辑。

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

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

相关推荐
疯狂的程序猴1 小时前
苹果iOS应用签名与上架App Store完整指南包括注意事项
后端
回家路上绕了弯1 小时前
生产环境服务器变慢?从应急到根因的全流程诊断处理指南
分布式·后端
小胖霞1 小时前
Node+Express+MySQL 后端生产环境部署,实现注册功能(三)
前端·后端
aiopencode2 小时前
抓包技术全面指南:原理、工具与应用场景
后端
该用户已不存在2 小时前
Gemini 3.0 发布,Antigravity 掀桌,程序员何去何从?
后端·ai编程·gemini
aiopencode2 小时前
软件苹果商城上架的流程与团队协作模式 一个项目从开发到发布的完整经历
后端
yeyong2 小时前
playwright的调试模式,方便调试selector, locator语法及查找效果
后端
鹿里噜哩2 小时前
Spring Authorization Server 打造认证中心(一)项目搭建/集成
java·后端·spring
汤姆yu2 小时前
基于springboot的智慧家园物业管理系统
java·spring boot·后端