本文目录导读:

- 目录导读
- 分页之痛:为什么原生MyBatis分页让人抓狂?
- 插件登场:PageHelper核心原理与执行流程解析
- 实战案例:Spring Boot集成PageHelper完整步骤
- 进阶技巧:多数据源分页、性能优化陷阱
- 常见问答:面试官最爱问的5个分页问题
目录导读
- 分页之痛:为什么原生MyBatis分页让人抓狂?
- 插件登场:PageHelper核心原理与执行流程解析
- 实战案例:Spring Boot集成PageHelper完整步骤(含代码)
- 进阶技巧:多数据源分页、动态表名、性能优化陷阱
- 常见问答:面试官最爱问的5个分页问题
分页之痛:为什么原生MyBatis分页让人抓狂?
在Java企业级开发中,列表分页是最基础的需求,但直接使用MyBatis原生分页时,开发者往往陷入两难:
- 物理分页:需要手写
LIMIT #{offset}, #{pageSize},每个SQL都要拼接,且无法复用。 - 内存分页:使用
RowBounds,看似简单,实则将所有数据加载到内存再截取,数据量大时直接OOM。
更棘手的是,当业务复杂到需要关联查询、子查询时,手动计算总记录数(SELECT COUNT(*))容易与主查询条件不一致,导致分页数据错乱。分页插件正是为解决这类痛点而生。
插件登场:PageHelper核心原理与执行流程解析
PageHelper是目前最主流的MyBatis分页插件,其核心机制基于MyBatis的拦截器(Interceptor),它拦截Executor.query()方法,在SQL执行前动态改写:
执行流程三步走:
- ThreadLocal存储分页参数:调用
PageHelper.startPage(pageNum, pageSize)后,参数暂存于当前线程。 - SQL智能改写:拦截器检测到ThreadLocal中有分页参数,则解析原SQL,自动生成:
- 分页SQL:方言适配(MySQL自动加
LIMIT,Oracle自动加ROWNUM)。 - Count SQL:自动优化为
SELECT COUNT(0),去除多余ORDER BY。
- 分页SQL:方言适配(MySQL自动加
- 结果封装:将查询结果包装为
PageInfo对象,包含页码、总条数、总页数等元数据。
关键优势:对业务代码零侵入,且支持强类型安全的分页对象。
实战案例:Spring Boot集成PageHelper完整步骤
场景模拟:电商后台用户管理列表,需要按创建时间倒序分页展示,并返回用户及其所属角色名称(联表查询)。
步骤1:引入依赖(Maven)
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
步骤2:配置YML(关键参数)
pagehelper: helper-dialect: mysql # 指定数据库方言 reasonable: true # 页码越界时自动回拨(如查第100页但只有5页,则查第5页) support-methods-arguments: true # 支持接口参数直接传入pageNum/pageSize
步骤3:编写Mapper接口(多表联查)
public interface UserMapper {
// 联表查询用户及角色,XML中编写SQL
List<UserVO> selectUserWithRole(@Param("keyword") String keyword);
}
对应的XML SQL:
<select id="selectUserWithRole" resultType="com.example.vo.UserVO">
SELECT u.id, u.username, u.create_time, r.role_name
FROM t_user u
LEFT JOIN t_role r ON u.role_id = r.id
<where>
<if test="keyword != null and keyword != ''">
AND u.username LIKE CONCAT('%', #{keyword}, '%')
</if>
</where>
ORDER BY u.create_time DESC
</select>
步骤4:Service层调用(核心模式)
public PageInfo<UserVO> getUsers(int pageNum, int pageSize, String keyword) {
// 1. 启动分页(必须在查询语句之前)
PageHelper.startPage(pageNum, pageize);
// 2. 执行查询(此时会被插件拦截改写)
List<UserVO> userList = userMapper.selectUserWithRole(keyword);
// 3. 封装分页结果
return new PageInfo<>(userList);
}
运行效果:
- 当
pageNum=2, pageSize=10时,自动生成:- Count SQL:
SELECT COUNT(0) FROM t_user u LEFT JOIN t_role r ON ... WHERE u.username LIKE '%张%' - 数据SQL:
SELECT ... LIMIT 10, 10
- Count SQL:
- 前端可轻松从
PageInfo获取总条数、总页数、是否首页/末页。
进阶技巧:多数据源分页、性能优化陷阱
陷阱1:startPage后必须紧跟查询
// 错误写法 PageHelper.startPage(1, 10); User user = new User(); // 中间掺杂其他操作 List<User> list = userMapper.selectAll(); // 分页无效 // 正确做法:startPage后直接调用Mapper方法
陷阱2:嵌套查询导致Count语句错误
若SQL中使用了UNION或FOR UPDATE,需手动指定countSuffix:
PageHelper.startPage(1, 10, true).setCountSuffix("_COUNT");
// Mapper中定义 <select id="selectAll_COUNT"> SELECT COUNT(*) FROM ... </select>
陷阱3:多数据源下方言冲突
若存在MySQL和Oracle双数据源,需在@DataSource切换前强制指定方言:
PageHelper.getLocalPage().setDialect(new MySqlDialect()); // 或者全局关闭自动识别,yml中配置 helper-dialect: mysql
性能优化最佳实践
- 严禁大字段分页:若查询列包含
TEXT或BLOB,建议先查主键分页后再回表。 - Count查询优化:PageHelper默认生成的count语句会去除
ORDER BY,但对复杂JOIN,可手动覆盖count SQL以提升速度。
常见问答:面试官最爱问的5个分页问题
Q1:PageHelper的原理是否会破坏MyBatis一级缓存?
A:不会,PageHelper只改写SQL,不涉及缓存,但需注意startPage后查询完,自动清除ThreadLocal中的分页参数,避免线程池复用导致数据错乱。
Q2:为什么我用PageInfo获取total时总是多了一次count查询?
A:这是正常现象,PageHelper总是执行一次count查询来获取总数,若追求极致性能,可通过PageHelper.startPage(1, 10, false)禁用count查询,此时total为0。
Q3:分页插件能处理LEFT JOIN产生的重复记录吗?
A:不能,这是SQL本身的问题,需通过DISTINCT或GROUP BY消除重复,分页插件只是基于你提供的最终结果集进行物理分页。
Q4:在Spring事务中,分页查询回滚会怎样? A:完全安全,分页插件只操作SQL语句生成,不参与事务管理,若事务回滚,查询结果也会一并回滚,无副作用。
Q5:分页插件在大数据量(百万级)下性能如何?
A:物理分页性能远优于内存分页,但LIMIT 100000, 10这种深分页依然很慢,推荐游标分页(基于WHERE id > #{lastId})或者用子查询优化深分页。