分页查出来的数据总少几条?可能是 MyBatis 后置过滤的坑

前言

上周排查了一个线上问题:一个列表查询接口,分页参数传了每页 10 条,但前端渲染出来有时候只有 6 条、有时候 8 条,偶尔又是完整的 10 条,没有任何报错。

如果你也遇到过这种 "分页返回数据不满页" 的诡异现象,大概率是踩了同一个坑------分页查询后的 Java 内存过滤

问题现象

前端请求:

json 复制代码
{
  "pageNum": 1,
  "pageSize": 10,
  "areacode": ""
}

期望返回 10 条数据,实际返回了 6 条。没有报错,没有异常日志。

根因分析

先看典型的错误代码流程:

java 复制代码
// Controller 层
public Result<List<DataVO>> queryList(@RequestBody QueryReq req) {
    PageHelper.startPage(req.getPageNum(), req.getPageSize());
    List<DataDO> list = mapper.selectList(req);
    
    // 后置过滤:对分页结果做内存过滤
    List<DataDO> filtered = list.stream()
        .filter(item -> allowedAreas.contains(item.getAreacode()))
        .collect(Collectors.toList());
    
    return Result.success(filtered);
}

这里的问题在于:

PageHelper 的工作时序PageHelper.startPage() 调用后,拦截的是紧接着的第一条 SQL,在其上拼接 LIMIT 10 OFFSET 0。此时分页已经发生,数据库返回了 10 行数据。

Service 层的二次筛选 :接下来 stream().filter() 对已分页的结果集做 areacode 白名单过滤------10 行中有 4 行 areacode 不在允许范围内,被过滤掉了,最终只剩 6 行。

前端的预期:前端传了 pageSize=10,期望每页返回 10 条数据。但 Service 层的后置过滤让实际返回数小于 pageSize,导致表格出现"空洞",用户体验很差。

为什么开发者容易踩这个坑

  1. 开发直觉蒙蔽:"过滤是展示层的调整"------通常这样想,但分页查询中过滤应该是数据层的事
  2. PageHelper 的隐式行为startPage() 的拦截是对"开发者不可见"的,容易忘记它的分页已经生效
  3. 小数据量下不易暴露:如果前几页恰好过滤后仍然有 10 条,问题不会出现。但在数据稀疏或白名单较窄的情况下就会间歇性触发

正确做法

方案一:过滤条件前移到 SQL 层面(推荐)

java 复制代码
// 构建查询条件时就把过滤逻辑拼入 WHERE 子句
QueryCriteria criteria = new QueryCriteria();
criteria.setPageNum(req.getPageNum());
criteria.setPageSize(req.getPageSize());
criteria.setAreacodeList(allowedAreas);   // 在 SQL 层面过滤

PageHelper.startPage(req.getPageNum(), req.getPageSize());
List<DataDO> list = mapper.selectList(criteria);
// 此时不需要再过滤,list 里的数据已经满足条件

对应的 Mapper XML:

xml 复制代码
<select id="selectList" resultType="DataDO">
    SELECT * FROM biz_data
    <where>
        <if test="areacodeList != null and areacodeList.size > 0">
            AND areacode IN
            <foreach collection="areacodeList" item="code" open="(" separator="," close=")">
                #{code}
            </foreach>
        </if>
    </where>
    ORDER BY create_time DESC
</select>

方案二:过滤条件依赖另一张表时,用子查询

如果白名单不能通过参数传入,而是需要关联另一张表判断:

xml 复制代码
<select id="selectList" resultType="DataDO">
    SELECT d.* FROM biz_data d
    <where>
        AND EXISTS (
            SELECT 1 FROM user_area_permission p
            WHERE p.user_id = #{userId}
            AND p.areacode = d.areacode
        )
    </where>
    ORDER BY d.create_time DESC
</select>

关键原则:让分页拦截发生在过滤之后的结果集上,而不是反过来。

如何排查此类问题

当你发现分页返回的数据不足 pageSize 时,按这个顺序排查:

  1. 检查 DAO 层方法返回后,Service 层是否有 stream().filter()for + if 跳过 等后置过滤逻辑
  2. 检查 Mapper XML 的 WHERE 条件是否已经包含了所有过滤条件
  3. 注意 PageHelper.startPage()mapper.selectList() 之间不要有任何查询操作,否则分页拦截会命中错误的 SQL

总结

对比项 错误做法 正确做法
过滤位置 Service 层(分页后) SQL 层(分页前)
代码形式 stream().filter() WHERE IN / EXISTS 子查询
结果 返回数据可能小于 pageSize 返回数据始终等于 pageSize(数据充足时)
排查难度 难,无报错日志 无此问题

这个坑的本质一句话总结:PageHelper 分页是对 SQL 结果集切分,后置过滤发生在切分之后,两者不能共存。

如果你也遇到过类似的问题,欢迎在评论区聊聊你是怎么排查的。

相关推荐
程序员清风6 分钟前
聊聊怎么缓解找工作的焦虑感?
java·后端·面试
不一样的少年_23 分钟前
明明做了很多事,为什么简历看起来还是没含金量?
前端·后端·招聘
newerp29 分钟前
Golang 函数调用的瞬间:栈帧、拷贝栈与逃逸分析
后端·程序员·go
会编程的吕洞宾29 分钟前
LangChain4j RAG 分块策略实战:检索质量提升 80% 的 Chunking 调优全解
java·人工智能·后端
newerp29 分钟前
Golang 网络轮询器 netpoll:把阻塞 IO 变成事件通知
后端·程序员·go
万物智能32 分钟前
HDMI 输出—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
后端·架构
Cache技术分享1 小时前
516. Java 方法句柄 - 转换(进阶篇·下)
前端·后端
妙码生花1 小时前
全网 8k star 的 BuildAdmin 正式发布 Golang 版本,这次我们在CRUD赛道杀死了比赛。
前端·后端·ai编程
Zane19941 小时前
复制算法明明要浪费一半内存,为什么新生代还偏偏用它
java·后端
写代码像蔡徐抻1 小时前
三年了,AI为何还没有抢走程序员饭碗?
前端·后端·面试