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(内存溢出),影响系统稳定性。

相关推荐
用户21991679703916 分钟前
使用Agent Framework进行多Agent工作流编排
后端
serendipity_hky13 分钟前
【go语言 | 第5篇】channel——多个goroutine之间通信
开发语言·后端·golang
zhaorong16 分钟前
RabbitMQ发布订阅模式同一消费者多个实例如何防止重复消费?
后端
开心猴爷18 分钟前
提升 iOS 应用安全审核通过率的一种思路,把容易被拒的点先处理
后端
我家领养了个白胖胖20 分钟前
Prompt、格式化输出、持久化ChatMemory
java·后端·ai编程
全栈老石25 分钟前
别再折腾端口转发了:使用 Cloudflare Tunnel 优雅地分享你的 localhost
前端·后端·全栈
Java编程爱好者28 分钟前
是猫踩键盘还是乱码?不,这是你刚写的正则表达式
后端
源代码•宸29 分钟前
分布式缓存-GO(简历写法、常见面试题)
服务器·开发语言·经验分享·分布式·后端·缓存·golang
LaughingDangZi43 分钟前
vue+java分离项目实现微信公众号开发全流程梳理
java·前端·后端