PageHelper分页原理解析:从源码到MySQL方言实现
一、引言
分页查询是Web开发的必备功能,MyBatis生态中的PageHelper以其简单易用的特性广受欢迎。本文将从源码层面(v5.3.2)解析PageHelper的分页实现机制,结合MySQL方言展示完整的执行链路。
二、核心实现原理
1. 插件初始化
PageHelper通过MyBatis插件机制注册拦截器
2. 分页参数设置
PageHelper.startPage()方法触发分页:
public static <E> Page<E> startPage(int pageNum, int pageSize) {return startPage(pageNum, pageSize, DEFAULT_COUNT);
}// 本质是通过ThreadLocal存储分页参数
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {Page<E> page = new Page<>(pageNum, pageSize, count);setLocalPage(page); // ThreadLocal保存return page;
}3. SQL拦截过程
核心拦截逻辑(关键代码精简):
public Object intercept(Invocation invocation) throws Throwable {// 1. 获取分页参数Page page = getLocalPage();// 2. 生成COUNT查询SQLif (page.isCount()) {count(page, mappedStatement, parameterObject, boundSql);}// 3. 生成分页SQL(MySQL方言)String pageSql = dialect.getPageSql(originalSql, page, page.getPageSizeKey());// 4. 反射修改BoundSql中的SQLField sqlField = boundSql.getClass().getDeclaredField("sql");sqlField.setAccessible(true);sqlField.set(boundSql, pageSql);// 5. 执行修改后的SQLreturn invocation.proceed();
}4. MySQL方言处理
MySqlDialect生成LIMIT子句:
public class MySqlDialect extends AbstractHelperDialect {public String getPageSql(String sql, Page page, String orderBy) {StringBuilder sqlBuilder = new StringBuilder(sql.length() + 20);sqlBuilder.append(sql);sqlBuilder.append(" LIMIT ?, ?");return sqlBuilder.toString();}// 参数处理逻辑public Object processPageParameter(...){paramMap.put("pageNum", page.getStartRow());paramMap.put("pageSize", page.getPageSize());}
}三、执行流程详解(以pageNum=2, pageSize=10为例)
-  参数设置阶段: - startPage(2, 10)创建Page对象并存入ThreadLocal
- Page对象计算偏移量:offset = (2-1)*10 = 10
 
-  SQL拦截阶段: - 原始SQL:SELECT * FROM user
- 改写后SQL:SELECT * FROM user LIMIT 10, 10
 
- 原始SQL:
-  参数绑定阶段: - 设置PreparedStatement参数: pstmt.setInt(1, 10); // offset pstmt.setInt(2, 10); // pageSize
 
- 设置PreparedStatement参数: 
-  结果封装阶段: // Page继承ArrayList Page<User> pageResult = (Page<User>) resultList; pageResult.setTotal(100); // 总记录数
-  PageInfo构建: new PageInfo<>(pageResult).getTotalPages(); // 计算总页数=10
四、关键设计亮点
-  ThreadLocal线程隔离: protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();
-  自动方言识别: <!-- 根据jdbcUrl自动识别 --> <property name="helperDialect" value="mysql"/>
-  智能COUNT优化: SELECT COUNT(0) FROM (原查询SQL) tmp_count
五、最佳实践建议
-  避免深分页: -- 页码过大时建议改用游标分页 SELECT * FROM user WHERE id > #{lastId} LIMIT 10
-  参数校验配置: PageHelper.startPage(0, 10); // 自动修正为pageNum=1
-  特殊查询处理: PageHelper.startPage(1, 10).disableCount(); // 不执行COUNT查询
六、总结
PageHelper通过MyBatis插件机制实现物理分页,其核心在于:
- ThreadLocal存储分页上下文
- 动态改写SQL语句
- 多方言支持体系
- 自动COUNT查询优化
结合MySQL的LIMIT语法特性,PageHelper在保证性能的同时提供了简洁的开发体验。理解其实现原理有助于避免深分页等常见问题,更好地发挥分页功能的价值。
