MyBatis Plus 分页插件失效?别再手写 limit 了!一文彻底搞懂 MP 分页原理与正确用法

18 阅读4分钟

最近在帮几个朋友 review 毕业设计代码时,发现一个高频问题:

“为什么 IPage 的 total 字段总是 0?数据能查出来,但总条数和页数都不对!”

一查代码,果不其然——分页插件没注册

这其实是个“经典陷阱”:MyBatis Plus(MP)的分页功能看似开箱即用,但如果你跳过了关键配置,它会静默退化为普通查询,既不报错,也不提醒,导致前端分页组件直接瘫痪。

今天,我就带大家从原理到实战,彻底搞懂 MP 分页的正确打开方式,并分享几个生产环境踩过的深坑。


一、为什么 MyBatis Plus 分页值得用?

传统分页你需要:

  1. 手写 SELECT COUNT(*) FROM table WHERE ...
  2. 再写 SELECT * FROM table WHERE ... LIMIT offset, size
  3. 后端手动封装 totalpagescurrent 等字段

而 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 开发效率的利器。
但它不是“魔法”,需要你正确配置、理解原理、规避陷阱

记住这个口诀:

“一分页,二插件,三查源,四防坑。”

把这篇文章加入书签,下次分页出问题,直接对照检查!

我是「不想打工的码农」,如果你觉得有收获,欢迎点赞、收藏、关注!