一、引言
分页查询是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查询SQL
if (page.isCount()) {
count(page, mappedStatement, parameterObject, boundSql);
}
// 3. 生成分页SQL(MySQL方言)
String pageSql = dialect.getPageSql(originalSql, page, page.getPageSizeKey());
// 4. 反射修改BoundSql中的SQL
Field sqlField = boundSql.getClass().getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, pageSql);
// 5. 执行修改后的SQL
return 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在保证性能的同时提供了简洁的开发体验。理解其实现原理有助于避免深分页等常见问题,更好地发挥分页功能的价值。