PageHelper分页的原理是什么?

1 个回答

  • 贝贝
    垆边人似月,皓腕凝霜雪。
    **PageHelper** 是 MyBatis 提供的分页插件,主要通过拦截 MyBatis 的执行器来动态修改 SQL,从而实现 **物理分页**。 ### **1. 核心原理** 1. **ThreadLocal 存储分页参数** - 当调用 `PageHelper.startPage(int pageNum, int pageSize)` 时,分页参数(如页码、每页大小等)会被存储到 **ThreadLocal** 中,确保分页参数在当前线程中是安全的。 **代码示例**: ```java PageHelper.startPage(1, 10); // 设置当前页为第1页,每页显示10条记录 ``` 2. **拦截 MyBatis 的 `query` 方法** - PageHelper 通过 MyBatis 的插件机制,拦截 `Executor` 的 `query` 方法。在方法执行前,通过 **ThreadLocal** 获取分页参数,并根据参数修改 SQL 语句。 3. **动态修改 SQL** - PageHelper 会在原始 SQL 末尾动态拼接 `LIMIT` 子句。 - **示例**: 原始 SQL: ```sql SELECT * FROM user; ``` 动态修改后: ```sql SELECT * FROM user LIMIT ?, ?; ``` - `?` 的值是分页参数,分别表示起始位置 (`offset`) 和返回记录数 (`limit`)。 4. **清除 ThreadLocal** - 分页操作完成后,PageHelper 会清除 **ThreadLocal** 中的分页参数,防止线程复用时产生脏数据。 ### **2. 工作流程** 1. **调用 `startPage` 方法** - 在执行查询之前调用 `PageHelper.startPage(pageNum, pageSize)` 方法,将分页参数存储到当前线程的 **ThreadLocal** 中。 2. **执行 SQL 拦截** - MyBatis 执行查询时,会调用 `Executor.query` 方法。 - PageHelper 的拦截器捕获到 `query` 方法的调用,通过 **ThreadLocal** 获取分页参数。 3. **修改 SQL** - PageHelper 根据分页参数计算 `LIMIT` 子句的 `offset` 和 `limit`,然后动态修改 SQL。 4. **执行查询** - 修改后的 SQL 被发送到数据库执行,返回分页结果。 5. **清理资源** - 查询结束后,清空 **ThreadLocal** 中的分页参数。 ### **3. 分页 SQL 生成** PageHelper 通过分页算法计算 `LIMIT` 子句的两个核心参数: - **`offset`(起始位置)**: ```java offset = (pageNum - 1) * pageSize; ``` - **`limit`(返回记录数)**: ```java limit = pageSize; ``` **示例**: 对于分页参数: - 当前页码:`pageNum = 3` - 每页大小:`pageSize = 10` 生成的 SQL: ```sql SELECT * FROM user LIMIT 20, 10; ``` - `20` 是起始位置(`offset`)。 - `10` 是返回的记录数。 ### **4. PageHelper 的核心实现** PageHelper 的核心逻辑在 **`SqlUtil` 类** 中,拦截器会在 `Executor.query` 方法中进行 SQL 改写。 #### **SQL 改写核心代码** ```java @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取当前线程的分页参数 Page<?> page = PageMethod.getLocalPage(); if (page != null) { // 2. 修改 SQL,拼接 LIMIT 子句 BoundSql boundSql = ms.getBoundSql(parameter); String originalSql = boundSql.getSql(); String limitSql = originalSql + " LIMIT ?, ?"; // 替换 SQL MetaObject metaObject = SystemMetaObject.forObject(boundSql); metaObject.setValue("sql", limitSql); // 3. 设置 LIMIT 的参数 setParameters(ps, mappedStatement, boundSql, parameterObject, page); } return invocation.proceed(); } ``` ### **5. 使用步骤** #### **5.1 添加依赖** 在项目中引入 PageHelper 的依赖。 ```xml <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.1</version> </dependency> ``` #### **5.2 配置分页插件** 在 MyBatis 配置中注册 PageHelper 插件。 **示例**: ```java @Configuration @MapperScan("com.example.mapper") public class MyBatisConfig { @Bean public PageInterceptor pageInterceptor() { return new PageInterceptor(); } } ``` #### **5.3 使用分页查询** 在代码中调用 `startPage` 方法设置分页参数,然后执行查询。 **示例代码**: ```java @Autowired private UserMapper userMapper; public PageInfo<User> getUsers(int pageNum, int pageSize) { // 设置分页参数 PageHelper.startPage(pageNum, pageSize); // 执行查询 List<User> users = userMapper.selectAll(); // 将结果封装为 PageInfo 对象 return new PageInfo<>(users); } ``` ### **6. 特点** #### **6.1 优点** - **易用性**:只需调用 `startPage` 方法,无需手动拼接分页 SQL。 - **无侵入性**:对业务逻辑无侵入,完全基于 MyBatis 的拦截机制。 - **支持多数据库**:自动根据数据库类型生成相应的分页 SQL。 #### **6.2 缺点** - **性能问题**: - 如果数据量过大,分页性能可能受限于数据库。 - 需要合理设置索引,避免全表扫描。 - **线程安全性**: - 由于使用 **ThreadLocal** 存储分页参数,必须确保在同一线程内调用 `startPage` 和查询操作。 ### **7. 总结** | **步骤** | **描述** | |---------------------------|---------------------------------------------------------------------------------------------| | **存储分页参数** | 调用 `PageHelper.startPage(pageNum, pageSize)`,将分页参数存储到 ThreadLocal。 | | **SQL 拦截** | 拦截 MyBatis 的 `Executor.query` 方法,在查询前动态修改 SQL。 | | **SQL 修改** | 根据分页参数在原始 SQL 末尾拼接 `LIMIT` 子句,实现物理分页。 | | **查询执行** | 修改后的 SQL 被发送到数据库执行,返回分页结果。 | | **清除分页参数** | 查询完成后,清空 ThreadLocal,避免线程复用时出现脏数据。 | PageHelper 的分页原理是通过 **ThreadLocal + SQL 拦截器** 实现分页逻辑,操作简单且灵活,适合大多数分页场景。
    编辑于 2024-12-12 19:42 •IP属地 美国
    举报
    评论
    收藏