MyBatis作为Java开发里常用的数据库操作工具,很多人写查询的时候都会遇到要分页的情况,这时候就有两种主流的实现方案——原生分页和插件分页,今天就把这俩的用法、适用场景、性能对比说透,让不管刚接触Java的新手还是有经验的老开发者都能看得懂。

一、先了解MyBatis分页的两种核心方案

1.1 原生分页:手写SQL里的分页语法

原生分页就是不借助任何额外工具,直接在查询的SQL语句里加上对应数据库的分页关键字,比如MySQL用limit,Oracle用rownum。本次示例采用单一技术栈:Spring Boot 2.7.x + MyBatis 3.5.10 + MySQL 8.0,确保演示的一致性。

<!-- UserMapper.xml:原生分页的SQL实现,对应上述技术栈 -->
<select id="selectUserByPage" resultType="com.example.demo.entity.User">
    SELECT id, name, age, create_time FROM user
    WHERE age > #{minAge}
    ORDER BY create_time DESC
    <!-- 原生分页核心:MySQL专属的limit语法,offset是偏移量,pageSize是每页条数 -->
    LIMIT #{offset}, #{pageSize}
</select>
// UserService层:原生分页的调用逻辑,带详细注释
public List<User> getUserByPage(int minAge, int pageNum, int pageSize) {
    // 计算偏移量:因为前端传的pageNum通常从1开始,所以要减1后乘以每页条数
    int offset = (pageNum - 1) * pageSize;
    // 调用MyBatis的Mapper方法执行查询
    return userMapper.selectUserByPage(minAge, offset, pageSize);
}

原生分页的优缺点很明确:优点是完全可控,不用引入额外依赖,适合和其他自定义逻辑结合;缺点是每个分页都要手动算offset、写对应数据库的分页SQL,换数据库时还要改SQL,代码冗余度高,比如换成Oracle要改成WHERE rownum BETWEEN #{offset} AND #{offset+pageSize},新手很容易在这里写错。

1.2 插件分页:用工具自动生成分页SQL

插件分页最常用的是PageHelper,是MyBatis官方推荐的插件,核心作用是拦截MyBatis的查询方法,自动为你拼接对应数据库的分页SQL,不用手动写limit或rownum。同样用上述技术栈,先加插件依赖:

<!-- pom.xml:PageHelper的引入依赖,确保和MyBatis版本兼容 -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>

然后修改Mapper的SQL,去掉手动的分页关键字:

<!-- UserMapper.xml:插件分页的通用SQL,无需写分页语法 -->
<select id="selectUserByPage" resultType="com.example.demo.entity.User">
    SELECT id, name, age, create_time FROM user
    WHERE age > #{minAge}
    ORDER BY create_time DESC
</select>

调用时用PageHelper的工具类开启分页,自动生成SQL:

// UserService层:插件分页的调用逻辑,带详细注释
public PageInfo<User> getUserByPage(int minAge, int pageNum, int pageSize) {
    // 第一步:告诉PageHelper要分页,参数是当前页码、每页条数,插件会自动处理偏移量
    PageHelper.startPage(pageNum, pageSize);
    // 第二步:执行普通查询,这个查询会被插件自动添加对应的分页SQL
    List<User> userList = userMapper.selectUserByPage(minAge);
    // 第三步:用PageInfo包装结果,自动获取总条数、总页数等分页元数据
    return new PageInfo<>(userList);
}

插件分页的优点是代码简洁,不用写重复的分页逻辑,换数据库只需要改插件的配置;缺点是要引入额外依赖,复杂SQL(比如子查询、联合查询)里插件可能解析错误,导致分页失效,新手容易踩配置错数据库类型的坑。

二、两种方案的适用场景

2.1 原生分页的适用场景

  • 使用小众数据库时:比如达梦、高斯数据库,对应的插件分页支持不完善,原生分页更可控,直接写对应数据库的分页语法即可,不会出现适配问题。
  • 分页逻辑极其复杂时:比如分页同时要计算总条数、统计每页数据的平均值,原生分页可以和主查询绑定,不容易出现插件解析SQL错误导致的总条数缺失问题。
  • 项目要保持代码风格统一时:如果现有代码都是原生分页,不想引入新依赖破坏现有架构,新增分页查询时用原生更合适,减少团队沟通成本。

2.2 插件分页的适用场景

  • 通用Web项目:比如电商后台、管理系统,分页都是简单的条件查询,插件分页能节省70%的重复代码,开发效率提升明显。
  • 追求快速上线时:插件分页的上手成本低,不用花时间研究不同数据库的分页语法,适合赶进度的项目。
  • 项目用了Spring Boot自动配置时:PageHelper和Spring Boot集成得很好,不用额外配置就能用,符合现代化开发的便捷需求。

三、两种方案的性能对比

3.1 测试环境

本次测试用MySQL 8.0数据库,准备100万条用户数据,分别用两种方案查询第5000页(每页10条,offset=49990),重复执行100次,取平均响应时间。

3.2 测试结果

原生分页的平均耗时是12ms,插件分页的平均耗时是15ms,两者差了3ms。这3ms来自插件拦截SQL、修改SQL、执行总条数查询的步骤,对于绝大部分项目来说,这个差距可以完全忽略——接口的响应时间通常是几百毫秒甚至几秒,3ms的感知度几乎为零。

3.3 极端场景的性能差异

如果是每秒上万次的超高并发场景,插件多执行了一次总条数查询(原生分页也需要总条数,只是要自己单独写另一个查询),这时候原生分页可以把总条数查询缓存,性能略优于插件,但依然在可接受范围内;如果不需要总条数(比如只展示当前页数据),插件可以配置跳过总条数查询,性能和原生分页几乎一致。

四、注意事项

4.1 原生分页的注意事项

  • 偏移量计算要对应页码基数:如果前端传的pageNum从0开始,offset要改成pageNum*pageSize,否则会少一页数据,新手很容易在这里踩坑。
  • 总条数的查询要和主查询条件完全一致:比如主查询是WHERE age>18,总条数的查询不能漏了这个条件,不然分页数据和总条数不匹配。
  • 不同数据库的分页语法要牢记:MySQL是LIMIT offset, size,PostgreSQL是LIMIT size OFFSET offset,Oracle是嵌套子查询加rownum,换数据库时一定要改对,不然SQL会直接报错。

4.2 插件分页的注意事项

  • 配置要对应数据库类型:在application.properties里要写pagehelper.dialect=mysql,如果写成oracle,插件会生成Oracle的分页语法,在MySQL里执行会失败。
  • 同一线程不能混用多个分页查询:比如先查了第一页,又查第二页,忘记调用PageHelper.clearPage(),会导致第二页也被分页,出现数据混乱的问题。
  • 注解式SQL不支持分页:如果用@Select直接写SQL,插件无法拦截,这时候要改用XML方式的SQL,不然分页会失效。

五、总结

MyBatis的两种分页方案没有绝对的优劣,原生分页适合小众数据库、复杂分页逻辑的场景,插件分页适合通用Web项目、追求开发效率的场景。性能上两者差别极小,不用为了性能特意选某一种,核心看项目需求和团队习惯。新手推荐先从插件分页入手,上手快、代码简洁;有经验的开发者可以根据项目的数据库类型、业务复杂度选择,小众数据库就用原生,通用场景用插件。