ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MyBatis-Plus分页查询total为0?分页拦截器未生效的根因与配置详解

MyBatis-Plus分页查询total为0?分页拦截器未生效的根因与配置详解 1. 表面现象每页有数据total 却是 0外包项目做到交付前验收最怕的不是功能没做完而是页面上那个看似不起眼的“总数”不对。当时客户那边测后台列表翻页没问题、每页也都有数据唯独分页控件显示“共 0 条”底部只有 1 页可以点。我一开始还以为是前端把 total 字段解析错了查了半天才发现后端 JSON 里 MyBatis-Plus 的 Page 对象 total 就是 0。项目用的是 MyBatis-Plus 的分页查询代码路径也确实是selectPage那为什么 total 始终不涨这篇文章我结合这次排查过程从现象、根因、配置到同类变种完整拆开讲适合正在被同样问题卡住的朋友直接对照排查。1.1 三种常见的“假分页”表现先说现象。我这里总结了三类特别容易混淆的表现如果你碰到的和下面某一条吻合可以先有个方向。第一种是total0但records里有数据而且翻页正常。这种情况最典型也最迷惑人。因为从用户视角来看列表内容是对的只是总数不对所以很多人第一反应是 UI 展示问题而不是后端分页逻辑问题。第二种是total0同时pages也变成了 0 或者 1。MyBatis-Plus 计算pages依赖total / sizetotal 一旦是 0pages 自然跟着出错。前端如果直接拿pages渲染总页数就会出现“只显示一页”的假象配合第一条总数据很难快速定位到根因。第三种是全集返回型错误。某些情况下没有分页插件介入时selectPage会把全部数据一次性查回来前端看到第一页一堆数据感觉分页“好像是好的”但实际上是内存里放着整个表的数据只是 UI 裁剪成了第一页。项目数据量小的时候根本发现不了等上线后数据涨到几十万接口直接超时或内存溢出。这个坑在外包项目里尤其危险因为验收环境通常只有几千条测试数据。1.2 先看代码路径有人把 total 弄丢在对象转换层在深入 MyBatis-Plus 机制之前有一个更基础但非常高发的点必须先排除你返回给前端的 Page 对象到底还是不是原来那个带着 total 的对象。我见过太多次这种写法ListUserVO voList userService.page(page, queryWrapper) .getRecords() .stream() .map(user - new UserVO(user)) .collect(Collectors.toList()); PageUserVO result new Page(pageNum, pageSize); result.setRecords(voList);这段代码的问题在于你新建了一个PageUserVO只把 records 塞了进去total、pages、current 都没复制。前端拿到的 total 必然是 0。这不是 MyBatis-Plus 的问题而是对象转换时把分页元数据弄丢了。另外还有一种情况是序列化层面的问题。如果你把 Page 对象转成 JSON 时只在 DTO 里定义了records、current、size字段而漏了total前端表现出来的症状一样是 total 为 0。解决办法也简单直接返回 MP 的Page对象本身或者转换 DTO 时把total、pages一起复制进去。这些“假 total0”排除干净之后才会轮到真正的嫌疑对象——分页拦截器。2. 根因定位分页拦截器没参与total 当然没人回填如果你确认了代码路径没问题、返回对象没问题、前端拿到的就是 MP 的Page对象但 total 还是 0那大概率是 MyBatis-Plus 的分页拦截器没有真正生效。2.1 一条分页 SQL 在 MP 内部实际被拆成了两条很多刚接触 MyBatis-Plus 的人会误以为selectPage只是一条带LIMIT的 SQL。实际上在标准配置下MyBatis-Plus 会把这一条“分页查询”拆成两件事执行一条COUNT查询算出满足条件的总记录数写回page.total再执行一条带LIMIT/OFFSET的分页查询把当前页数据写回page.records。这两个动作由PaginationInnerInterceptor来驱动。这个拦截器的工作流程大致是先读取方法参数里那个Page对象拿到页码和每页大小然后根据原始 SQL 生成 count SQL 并执行接着按数据库方言改写原始 SQL拼上LIMIT ? OFFSET ?之类的语句再执行。如果拦截器没有注册到 MyBatis 的执行链路上那么这两个动作都不会发生。total就只能是 Page 对象初始化时的默认值 0。这也解释了为什么列表能查到数据但total0——你执行的还是普通查询只是数据恰好放在了 records 里而额外统计总数的那一步被跳过了。2.2 验证方法看 SQL 日志里有没有那条 count排查时最直观的手段是开 SQL 日志。在 Spring Boot 配置里加上mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl然后重新请求一次分页接口观察控制台输出。如果拦截器生效你会看到类似这样的两条 SQL-- 第一条是 count 统计 SELECT COUNT(*) FROM user WHERE name ?; -- 第二条是实际分页查询 SELECT id, name, age FROM user WHERE name ? LIMIT ?, ?;如果只看到一条普通的SELECT完全没有COUNT语句或者根本连LIMIT都没有那就说明分页拦截器没有参与执行。这时候再去看配置类十有八九是缺少PaginationInnerInterceptor的注册。还有一种情况日志被项目里的日志框架吞掉了看不到输出。这时可以用断点法验证在PaginationInnerInterceptor的beforeQuery方法上打断点如果请求分页接口时根本不停下来说明拦截器没有挂上。另一个更笨但有效的办法把maxLimit设置成 1然后请求size20如果返回结果每页只有 1 条说明拦截器生效了如果还是 20 条那配置肯定没生效。2.3 版本换代带来的配置变化PaginationInterceptor 该退休了很多外包项目有一个特点代码是多个开发反复复制拼接出来的。你可能会在配置文件里看到一个Bean PaginationInterceptor以为这就是分页插件。这个类在 MyBatis-Plus 3.4.0 之前是主流写法但从 3.4.0 开始官方引入了新的组合式拦截器MybatisPlusInterceptor并在后续版本逐步弃用老的PaginationInterceptor。如果你项目使用的 MP 版本已经到 3.4 以上还在用老写法就可能出现分页配置“看似存在、实际不产生效果”的状况。有些时候老类仍然存在项目能编译能启动但分页行为已经不是预期那样有些版本里老类直接被移除启动直接报找不到类。无论哪种正确方向都是改用新版 APIConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }所以排查思路里一定要带一步确认你的pom.xml里 MyBatis-Plus 的版本号。版本不同配置写法完全不同。3. 正确的配置方式MybatisPlusInterceptor PaginationInnerInterceptor确定要往新版走之后配置本身并不复杂但有几个细节值得认真对待。3.1 最小配置与推荐配置最小可用配置就是上面那段一个MybatisPlusInterceptorBean里面 add 一个PaginationInnerInterceptor。但对于真实项目我更建议做一次完整配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(500L); paginationInterceptor.setOverflow(false); paginationInterceptor.setOptimizeJoin(true); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }注意我这里用的是DbType.MYSQL如果你的数据库是 PostgreSQL、Oracle、达梦或 SQL Server要换成对应的枚举值。这个参数不仅关系到分页 SQL 的生成也会影响 count SQL 的写法填错的话问题表现千奇百怪。3.2 几个参数的真实用途setMaxLimit(500L)用来限制单页最大查询条数。比如前端恶意传一个size100000没有这个限制的话LIMIT 100000可能直接把数据库连接拖死。设了之后MP 会自动把超过上限的 size 压回到配置值同时不影响 total 的统计。setOverflow(false)控制的是页数越界行为。当请求的页码超过总页数时如果设为trueMP 会把 current 修正到第一页如果设为false会继续向后查询最终返回一个空列表。对外包后台管理系统来说我一般建议设为false让多传页码的请求返回空数据而不是自动跳转这样接口行为更可控。setOptimizeJoin(true)是针对多表JOIN查询的 count 优化。默认情况下分页插件生成 count SQL 时会对原始 SQL 做简化把一些不必要的JOIN去掉或重写减少统计开销。如果因为某些特殊 SQL 导致自动生成的 count 有问题可以把这项关掉或在Page对象上单独指定 count 方法。3.3 配置完如何确认已经生效配置改完后不要只看接口不报错就认为好了。我习惯用三个步骤确认第一步再一次请求分页接口看控制台 SQL 日志里是否同时出现 count 和分页两条 SQL。两条都在说明拦截器已经接管。第二步测试total值是否与手动执行SELECT COUNT(*)的结果一致。如果一致说明计数逻辑正确如果不一致需要往后看 JOIN 等复杂场景。第三步写一个小测试方法验证pages计算。比如造 35 条数据每页 10 条预期 total35、pages4。当前端显示“共 35 条”而不是“共 0 条”时这个问题才算真正解决。4. 同一个坑的多种“马甲”自定义 SQL、多数据源与框架冲突total 为 0 的坑并不只在基础配置里出现。我这次项目排查完之后顺手把组里几个老外包项目都扫了一遍发现还有很多变种症状一样触发场景不同。这里集中说一下免得大家排完基础配置后又被下一个马甲坑住。4.1 自定义 XML 分页方法返回了 List 而不是 IPage外包项目里很少有纯单表 CRUD大多数列表查询都带着各种条件、关联和子查询所以很多团队会自己写 XML SQL然后配合Page参数做分页。常见写法是这样IPageUser selectUserPage(PageUser page, Param(name) String name);这样写是对的分页插件会识别IPage类型参数帮你在执行前改造 SQL并把 count 结果回填到 Page 对象。但还有一种常见错误写法// 错误示例 ListUser selectUserPage(PageUser page, Param(name) String name);返回值改成了ListSQL 里照样写查询执行时分页插件还是会尝试追加LIMIT来截断数据但你拿不到封装好的 total。因为调用方拿到的是List而统计数没有被封装到这个 list 里最终倒回到Page参数的 total 也可能因为框架内部逻辑而没有被正确回填。换句话说你分页分了个寂寞总数还是不知去向。所以用自定义 SQL 做分页时Mapper 方法返回值统一用IPageT或PageT。XML 里也不需要手写LIMIT分页插件会自动处理。如果非要手写LIMIT那插件是没法二次追加的total 一样可能为 0。4.2 多数据源下 dbType 不匹配count 一样会乱外包项目尤其是老项目里一套系统同时连两个数据库的情况非常常见。比如主库 MySQL辅助库 Oracle 或 PostgreSQL。如果你全局只配了一个PaginationInnerInterceptor(DbType.MYSQL)当某个分页查询走的是 Oracle 数据源时分页插件却按 MySQL 方言去拼 SQL轻则分页语法不对重则查询直接报错。即使不报错count SQL 的生成也可能和预期不一致total 自然拿不到。多数据源类型一致时问题还不大一个全局配置通常够用类型不一致时我建议按SqlSessionFactory维度分别配置不同的分页拦截器每个拦截器指定对应的DbType。具体到这个项目里哪怕你只配一个全局的也至少要在代码里通过DS确认当前线程走的是哪个库再决定要不要处理拦截器规则。这类的坑不好排查的点在于它不会稳定复现只有请求走到那个特定数据源时会出问题。而且日志里你看到的 SQL 未必是分页插件生成的很容易被误导到 SQL 本身的问题上。4.3 项目里同时存在 PageHelper 会怎样外包项目的依赖相当“丰富”有些老模块用 PageHelper新模块用 MyBatis-Plus两个框架共存于一个工程里。这个时候要注意PageHelper 本身也是一个 MyBatis 插件它会拦截 Executor 层的查询并尝试分页而 MyBatis-Plus 的分页拦截器也在这一层做手脚。两个插件叠加在一起最典型的表现是逻辑互相覆盖PageHelper 抢先做了 count 和 limit 处理MyBatis-Plus 的 Page 对象就没有机会被正确回填 total反过来也可能各处理各的导致分页结果数据错乱。我的建议是同一个项目里只保留一套分页方案。新代码用 MyBatis-Plus老代码能迁移就迁移不能迁移就把分页逻辑收敛在少数几个老模块里不要让两种分页框架流向同一个 Service 或同一个 Mapper。4.4 复杂 JOIN 的 count 不准问题还有一种“total 不是 0 但明显不对”的情况比如列表本身 10 条total 算出来 50 条。这种问题常见于多表 JOIN 且关联关系不是一对一的情况。分页插件有optimizeJoin优化但遇到特别复杂的 SQL自动生成的 count 还是可能失准。这时候比较稳的办法是给分页单独指定一个 count 查询方法。你可以单独在 Mapper 里写一个专用的 count SQL然后在执行分页时指定PageUser page new Page(pageNum, pageSize); page.setCountId(selectUserCount); userMapper.selectUserPage(page, name);这样分页插件就会优先使用你指定的 count 方法而不是自己拼一个。自定义 count SQL 写好后用真实数据验证一下和列表实际总数是否一致基本能堵住这个口子。5. 外包交付前的分页自测清单这次踩坑之后我把分页相关的问题整理成了一张自测清单项目交付前照着过一遍能省掉不少验收阶段的来回拉扯。排查项检查方式常见结果分页插件是否注册查看配置类中是否有MybatisPlusInterceptorBean未注册时 total0分页插件版本匹配对比pom.xml中 MP 版本和配置写法3.4 用旧写法时不生效dbType 是否正确与数据库实际类型对比不匹配时分页 SQL 异常mapper 方法返回类型查看自定义分页方法签名返回 List 时 total 可能一直为 0返回对象是否被重建检查代码中是否有new Page后手动复制 recordstotal 丢失count SQL 是否正确手动执行生成的 COUNT 语句统计JOIN 场景下可能统计重复是否存在分页框架冲突检查项目依赖中是否有 PageHelper 等插件互相覆盖导致 total 异常分页日志是否出现 count打开 SQL 日志请求分页接口无 count 说明插件未介入除了这张表还有一个具体场景也值得提外包项目上线前建议至少在数据量超过 10 万的表上做一次分页压测。很多分页问题在几千条数据时根本不会暴露一旦数据量上来拦截器没生效导致的“假分页”直接变成接口超时。如果你在测试环境只有几百条数据翻页看起来一切正常特别容易把这类问题漏掉。我个人的习惯是每次接到外包列表功能的第一件事不是急着写 Service而是先看一眼现有项目的 MP 配置和版本。这个动作花了五分钟但能避免后面花半天时间回头排查 total 为什么是 0。分页这种东西上层代码写得再花哨底层拦截器没生效一切都是白搭。
返回列表