MyBatis 是如何进行分页的?分页插件的原理是什么?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,通常意在考察以下几个层面:

  1. 基础 API 掌握:你是否了解 MyBatis 提供的原生分页方式(RowBounds)及其局限性。
  2. 实践与性能意识:你是否理解真正的生产级分页必须在数据库层面执行(物理分页),而非在应用内存中处理(逻辑分页),并清楚其性能影响。
  3. 框架扩展能力:你是否深入探究过 MyBatis 的插件(Interceptor)机制,这是理解主流分页插件(如 PageHelper)如何工作的核心。
  4. 源码探究与设计思想:面试官不仅仅想知道 “怎么用”,更是想通过此问题,评估你是否具备阅读主流框架源码、理解其扩展点设计思想的能力,这是区分普通开发者和资深开发者的关键。

核心答案

MyBatis 的分页主要分为两种方式:

  1. 原生/内存分页:使用 RowBounds 参数,它本质上是在 SQL 查询出所有结果后,在应用内存中进行截取,效率低下,不适用于大数据量
  2. 物理分页(推荐):通过 编写带有 LIMIT (MySQL) 或 ROWNUM (Oracle) 等数据库方言的 SQL,在数据库层面直接返回指定页的数据。这是生产环境的标准做法。

分页插件(如 PageHelper)的原理 正是为了自动化物理分页:它基于 MyBatis 的 插件(拦截器)机制,在执行目标 SQL 前,动态地修改原 SQL,为其添加上数据库特定的分页语句(例如将 SELECT * FROM user 改写为 SELECT * FROM user LIMIT 0, 10),并可能额外执行一个 COUNT 查询以获得总记录数。

深度解析

原理/机制:插件如何 “拦截” 并 “改写” SQL

  1. MyBatis 插件机制:MyBatis 允许你编写拦截器,在四大核心对象(Executor, StatementHandler, ParameterHandler, ResultSetHandler)的方法执行前后进行拦截。分页插件通常拦截 StatementHandler.prepare 方法。
  2. 动态代理与责任链:MyBatis 在启动时会为这些核心对象创建代理对象,所有插件按配置顺序形成一个拦截器链(InterceptorChain)。当方法被调用时,会按顺序执行插件的逻辑。
  3. 分页插件的核心步骤
    • 拦截时机:插件拦截 StatementHandler.prepare(Connection, Integer) 方法。
    • 识别分页参数:从线程上下文的 ThreadLocal 或方法参数中,获取前端传入的页码(pageNum)和每页大小(pageSize)。
    • 改写 SQL:使用 MetaObject(MyBatis 提供的反射工具类)获取到当前 StatementHandlerBoundSql 对象里的原始 SQL 字符串,然后根据配置的数据库方言(Dialect),使用字符串拼接或 AST 解析的方式,将其改写为包含分页子句的物理分页 SQL。
    • 执行 Count 查询(可选):如果需要获取总记录数,插件会使用同一个 Connection,先执行一条由原始 SQL 改写而来的 COUNT(*) 查询,并将结果存储起来供后续使用。

RowBounds 的原理与局限

// 示例:使用 RowBounds (不推荐用于生产)
List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.selectAll”, null, new RowBounds(10, 20)); // offset=10, limit=20

原理:MyBatis 的 DefaultResultSetHandler 在处理结果集时,会检查传入的 RowBounds。它虽然执行了 SELECT * FROM table 这样的全量查询,但在遍历 ResultSet 时,会跳过前面 offset 条记录,然后只读取 limit 条记录封装返回。 局限全量数据会从数据库传输到应用服务器,并在 JDBC 驱动和 MyBatis 层面进行遍历和丢弃,对数据库、网络和应用内存都造成巨大压力。

最佳实践与常见误区

  • 最佳实践
    1. 始终使用物理分页插件,如 PageHelper 或 MyBatis-Plus 的内置分页功能。
    2. 在查询 大表 时,如果 COUNT(主键) 仍然很慢,考虑不做精确计数,或使用异步加载、滚动加载(如 “加载更多”)等方案。
    3. 确保分页查询字段上有合适的索引,尤其是 ORDER BYWHERE 子句中的字段。
  • 常见误区
    1. 误以为 RowBounds 是高效分页:这是最典型的误解,必须明确其内存分页的本质。
    2. 忽略 COUNT 查询性能:在数据量极大或查询极复杂时,COUNT 查询本身可能成为瓶颈。需要根据业务场景权衡是否必须精确总数。
    3. 忘记清理线程变量:PageHelper 使用 ThreadLocal 存储分页参数。如果在异步或线程池环境下使用,必须在分页逻辑结束后调用 PageHelper.clearPage(),否则参数可能污染后续无关查询。

总结

MyBatis 分页的核心思想是借助插件机制,将内存分页转化为高效的数据库物理分页;其实现的关键在于通过拦截器动态改写 SQL,这是 MyBatis 可扩展性设计的一个经典体现。