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在保证性能的同时提供了简洁的开发体验。理解其实现原理有助于避免深分页等常见问题,更好地发挥分页功能的价值。