最近在帮几个朋友 review 毕业设计代码时,发现一个高频问题:
“为什么
IPage的total字段总是 0?数据能查出来,但总条数和页数都不对!”
一查代码,果不其然——分页插件没注册。
这其实是个“经典陷阱”:MyBatis Plus(MP)的分页功能看似开箱即用,但如果你跳过了关键配置,它会静默退化为普通查询,既不报错,也不提醒,导致前端分页组件直接瘫痪。
今天,我就带大家从原理到实战,彻底搞懂 MP 分页的正确打开方式,并分享几个生产环境踩过的深坑。
一、为什么 MyBatis Plus 分页值得用?
传统分页你需要:
- 手写
SELECT COUNT(*) FROM table WHERE ... - 再写
SELECT * FROM table WHERE ... LIMIT offset, size - 后端手动封装
total、pages、current等字段
而 MP 只需一行:
IPage<Student> page = studentMapper.selectPage(new Page<>(1, 10), queryWrapper);
返回对象自动包含:
records:当前页数据列表total:总记录数(非当前页数量!)pages:总页数current/size:当前页码与每页大小
前后端联调效率提升 50%+ ,尤其搭配 Vue/React 的分页组件时,几乎零转换成本。
二、正确姿势:三步实现专业分页
✅ 第一步:注册分页插件(90% 的人漏在这里!)
关键点:必须使用 MybatisPlusInterceptor(MP 3.4.0+ 推荐方式)
@Configuration
@MapperScan("com.example.mapper") // 替换为你的 mapper 包路径
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusAdapter adapter = new MybatisPlusAdapter();
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件,指定数据库类型(必须!)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
📌 注意:
- 旧版
PaginationInterceptor已废弃,新项目请勿使用。DbType必须显式指定,否则 MP 无法生成正确的分页 SQL(如 MySQL 用LIMIT,Oracle 用ROWNUM)。- 此配置全局生效,无需每个 Mapper 单独处理。
✅ 第二步:Service 层调用分页方法
public IPage<Student> getPage(int current, int size, String name) {
Page<Student> page = new Page<>(current, size);
QueryWrapper<Student> qw = new QueryWrapper<>();
if (StringUtils.isNotBlank(name)) {
qw.like("name", name);
}
return studentMapper.selectPage(page, qw); // MP 自动注入 COUNT + 分页
}
✅ 第三步:Controller 返回给前端
@GetMapping("/students")
public ResponseEntity<IPage<Student>> list(
@RequestParam(defaultValue = "1") int current,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String name) {
IPage<Student> page = studentService.getPage(current, size, name);
return ResponseEntity.ok(page);
}
前端(Vue3 + Element Plus)可直接消费:
const { records, total, current, size } = await api.getStudents({ current: 1, size: 10 });
// 直接绑定 el-pagination 的 total 和 currentPage
三、深度避坑:为什么你的分页 still 不 work?
❌ 坑 1:插件未注册 → 静默失败
这是最常见问题。MP 在未注册插件时,selectPage 会退化为 selectList,只返回第一页数据,且 total=0。
✅ 验证方法:
启动日志中搜索 MybatisPlusInterceptor,确认是否加载成功。
❌ 坑 2:自定义 XML SQL 导致 COUNT 失效
如果你在 Mapper XML 中写了:
<select id="selectPage">SELECT * FROM student WHERE ...</select>
MP 无法自动注入 COUNT 查询,因为 XML 覆盖了 MP 的默认行为。
✅ 解决方案:
- 优先使用
QueryWrapper构建动态 SQL - 若必须用 XML,需手动实现两条语句(不推荐),或改用
@Select注解 + MP 条件构造器
❌ 坑 3:多数据源环境下插件未生效
使用 dynamic-datasource-spring-boot-starter 时,分页插件需在主数据源配置中注册,或通过 @DS 注解确保查询走正确数据源。
四、性能与安全建议
🔒 1. 防止深度分页攻击
恶意请求 ?current=999999&size=10 会导致 LIMIT 9999990, 10,全表扫描拖垮数据库。
✅ 对策:
- 前端限制最大页码(如
current <= 1000) - 后端校验:
if (current > 1000) throw new IllegalArgumentException("页码过大");
⚡ 2. 大表分页优化
对于千万级表,COUNT(*) 本身就很慢。
✅ 进阶方案:
- 使用“游标分页”(基于 ID 或时间戳):
WHERE id > lastId ORDER BY id LIMIT 10 - 异步统计总数(如 Redis 缓存近似值)
📊 3. 监控慢查询
开启 MyBatis 日志,观察 MP 生成的 SQL 是否包含 COUNT 和 LIMIT:
-- MP 自动生成的两条 SQL
SELECT COUNT(*) FROM student WHERE name LIKE '%张%';
SELECT * FROM student WHERE name LIKE '%张%' LIMIT 0, 10;
五、结语
MyBatis Plus 的分页插件,是提升 CRUD 开发效率的利器。
但它不是“魔法”,需要你正确配置、理解原理、规避陷阱。
记住这个口诀:
“一分页,二插件,三查源,四防坑。”
把这篇文章加入书签,下次分页出问题,直接对照检查!
我是「不想打工的码农」,如果你觉得有收获,欢迎点赞、收藏、关注!