前言
在开发 Web 应用时,我们经常需要处理海量数据的展示问题。例如,在一个电商平台上,商品列表可能有成千上万条数据。如果我们一次性将所有数据返回给前端,不仅会导致页面加载缓慢,还会对数据库造成巨大压力。为了解决这个问题,分页查询应运而生。所谓分页查询,就是当查询结果数据较多时,采用按页显示的方法将数据分成若干个“页面”,每次只加载当前页面的数据,而不是一次性全部显示。通过分页可以有效减少单次查询的返回数据量,提升性能和用户体验。PageHelper 是国内非常优秀的一款开源的 MyBatis 分页插件,能够帮助我们快速实现分页功能。而且它支持基本主流与常用的数据库, 例如mysql、 oracle、mariaDB、 DB2、 SQLite、Hsqldb等,今天就给大家聊聊 PageHelper 这款分页插件。
一、PageHelper 概述
1.1 什么是 PageHelper
在开发过程中实现分页查询,我们可以使用 SQL 语句中添加 limit 关键字的方法实现分页查询。但是查询分页内容时,需要计算相关的分页信息和参数。而且无论什么业务其分页逻辑都是类似的,所以有框架帮助我们高效实现分页功能。PageHelper 是一个基于 MyBatis 的分页插件,它通过拦截 MyBatis 的执行器,在 SQL 语句执行前后动态添加分页逻辑来实现分页功能。通过简单的配置和调用,PageHelper 可以自动处理 SQL 查询中的分页逻辑,极大地简化了分页功能的开发过程,支持多种分页方式和结果集排序、筛选等操作。
- PageHelper 官网:https://pagehelper.github.io
- PageHelper 源码:https://gitee.com/free/Mybatis_PageHelper/
- PageHelper API:https://apidoc.gitee.com/free/Mybatis_PageHelper/
1.2 PageHelper 的工作原理
PageHelper 的工作原理主要依赖于拦截 MyBatis 的查询操作,在查询前设置分页参数,并在执行 SQL 语句时动态添加分页逻辑,从而实现分页查询。它通过修改当前执行的 SQL 语句来添加分页条件,执行添加了分页条件的 SQL 语句,最终返回分页后的结果集。此外,PageHelper 还提供了详细的配置选项和默认参数支持,如pagehelper.reasonable、pagehelper.defaultCount等,用户可以根据自己的需求进行配置。在整合PageHelper到项目中时,需要确保已经正确导入了MyBatis的依赖,并且按照官方文档的指引进行依赖的引入和配置。
总的来说,PageHelper 是一款功能强大且易于使用的 MyBatis 分页插件,它大大简化了分页查询的实现过程,提高了开发效率,是 Java 项目中实现分页功能的常用工具之一。
二、PageHelper 的基本使用
2.1 引入依赖
在使用 PageHelper 之前,需要在项目中引入 PageHelper 的相关依赖。如果使用的是 Maven,可以在 pom.xml 中添加以下依赖:
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>x.y.z</version>
</dependency>
若是 Spring Boot中集成 PageHelper,需要在 pom.xml 中添加以下依赖即可,无需额外配置,PageHelper 会自动集成 MyBatis。
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>x.y.z</version>
</dependency><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>x.y.z</version>
</dependency>
2.2 配置拦截器插件
在 MyBatis 配置文件中配置
在 MyBatis 的配置文件中(通常是 mybatis-config.xml ),添加 PageHelper 的插件配置,关键代码如下所示:
<plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 配置全局的分页参数 --><property name="helperDialect" value="mysql"/><property name="offsetAsPageNum" value="true"/><property name="rowBoundsWithCount" value="true"/></plugin>
</plugins>
在 Spring Boot中配置
Spring Boot 引入 starter 后自动生效,对分页插件进行配置时,通常可以通过配置文件 application.properties
或 application.yaml
中进行配置,关键代码如下所示:
pagehelper:helper-dialect: mysql # 配置分页插件的方言,即使用的什么数据库则就用什么数据库reasonable: true # 开启合理查询:即若超过最大页跳到最后一页,若查询-1页,默认查询第一页。support-methods-arguments: true # 通过 Mapper 接口方法的参数来传递分页参数params: count=countSql # 指定count查询的参数名称。
或者可以通过配置类定义设置相关的参数信息,关键代码如下所示:
import com.github.pagehelper.PageHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;@Configuration
public class MyBatisConfig {@Beanpublic PageHelper pageHelper() {PageHelper pageHelper = new PageHelper();Properties properties = new Properties();properties.setProperty("dialect", "Mysql");properties.setProperty("offsetAsPageNum", "true");properties.setProperty("rowBoundsWithCount", "true");pageHelper.setProperties(properties);return pageHelper;}
}
2.3 编写 Mapper 接口和 XML 文件
为了实现 PageHelper,定义一个实体类,关键代码如下所示:
@Data
public class SysUser {private Long id;private String userName;private String password;private String phone;private String realName;private String nickName;private String email;// 账户状态(1.正常 2.锁定 )private Integer status;// 性别(1.男 2.女)private Integer sex;// 是否删除(1未删除;0已删除)private Integer deleted;private Long createUserId;private Long updateUserId;private Date createTime;private Date updateTime;
}
编写相关 Mapper 接口和对应的 XML 文件,这些文件应该包含需要进行分页查询的 SQL 语句。注意,这里不需要特别修改 SQL 语句以支持分页,因为 PageHelper 会自动处理。假设我们有一个 UserMapper 接口,用于查询用户数据,关键代码如下所示:
@Mapper
public interface SysUserMapper {List<SysUser> selectUserList(Page<SysUser> page);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.dllwh.mybatis.mapper.SysUserMapper"><select id="selectUserList" resultType="com.dllwh.mybatis.model.SysUser">SELECT * FROM sys_user</select>
</mapper>
2.4 实现分页查询
在 Service 层或 Controller 层中,我们可以通过 PageHelper 提供的方法来进行分页参数的设置和获取分页结果。PageHelper 的核心方法是 PageHelper.startPage()
,它的作用是为当前线程开启分页上下文,并在接下来的查询中拦截 SQL,添加分页参数。关键代码如下所示:
public interface UserService {PageInfo<User> selectUserList(int pageNum, int pageSize);
}@Service
public class UserServiceImpl implements UserService{@Resource private SysUserMapper sysUserMapper;public PageInfo<User> selectUserList(int pageNum, int pageSize) {// 这句代码要放在查询 mapper 语句的前面,用于对数据库查询 SQL 设置分页参数PageHelper.startPage(pageNum, pageSize);// 执行查询操作List<User> userList = sysUserMapper.selectUserList(null);// 将查询到的结果对象封装到pageInfo中,可以查看分页的各种数据return new PageInfo<>(userList);}
}
PageHelper 利用 MyBatis 的插件机制拦截查询语句,在查询 SQL 中自动加入分页语法,如 MySQL 的 LIMIT
或 Oracle 的 ROWNUM
,并执行两次 SQL 查询:
- 查询总记录数:执行
SELECT COUNT(*) FROM ...
获取满足条件的记录总数。 - 查询分页数据:在原始查询 SQL 后追加分页条件。
2.5 返回分页结果
在 Controller 层中,我们可以将分页结果返回给前端,关键代码如下所示:
@RestController
@RequestMapping("/user")
public class UserController {@Resource private UserService userService;@GetMapping("/list")public PageInfo<User> getUsersList(int pageNum, int pageSize) {return userService.getUsersList(pageNum, pageSize);}
}
从服务层获取到分页数据后,就可以在前端页面上进行展示,并添加分页导航条等控件来控制分页。
三、PageHelper 的核心功能
分页对象 Page
Page 是一个接口,它包含分页数据以及一些基本的分页信息(如总记录数、当前页等)。当使用 PageHelper 进行分页查询时,查询结果会被自动封装到一个实现了 Page 接口的对象中。
public class Page<E> extends ArrayList<E> implements Closeable {// 页码,从1开始private int pageNum;// 页面大小private int pageSize;// 起始行private long startRow;// 末行private long endRow;// 总数private long total;// 总页数private int pages;// 包含count查询private boolean count = true;// 分页合理化private Boolean reasonable;// 当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果private Boolean pageSizeZero;// 进行count查询的列名private String countColumn;// 排序private String orderBy;// 只增加排序private boolean orderByOnly;// sql拦截处理private BoundSqlInterceptor boundSqlInterceptor;// 分页实现类,可以使用 {@link com.github.pagehelper.page.PageAutoDialect} 类中注册的别名,例如 "mysql", "oracle"private String dialectClass;// 转换count查询时保留查询的 order by 排序private Boolean keepOrderBy;// 转换count查询时保留子查询的 order by 排序private Boolean keepSubSelectOrderBy;// 异步count查询private Boolean asyncCount;
}
方法 | 默认值 | 简要说明 |
---|---|---|
List getResult() | emptyList | 获取分页后的数据列表 |
Long getTotal() | 0 | 获取列表总记录数 |
int getPageNum() | 1 | 获取当前页码 |
int getPageSize() | 10 | 每页显示条数,默认 10 |
boolean isLastPage() | 判断是否为第一页 | |
boolean isLastPage() | 判断是否为最后一页 | |
boolean hasPreviousPage() | 判断是否有上一页 | |
boolean hasNextPage() | 判断是否有下一页 | |
int getPrePage() | 获取上一页的页码。 | |
int getNextPage() | 获取下一页的页码。 |
分页信息封装类 PageInfo
我们在使用 MyBatis 分页时,不论是使用自动分页还是手动分页都会使用 PageInfo 对象作为承接介质。PageInfo 是 PageHelper 提供的用于封装分页结果的对象,包含完整的分页信息。它可以将分页查询结果和分页参数封装在一个对象中,便于传输和使用。
public class PageInfo<T> extends PageSerializable<T> {// 当前页private int pageNum;// 每页的数量private int pageSize;// 当前页的数量private int size;// 当前页面第一个元素在数据库中的行号(不常用)private long startRow;// 当前页面最后一个元素在数据库中的行号(不常用)private long endRow;// 总页数private int pages;// 前一页private int prePage;// 下一页private int nextPage;// 是否为第一页private boolean isFirstPage = false;// 是否为最后一页private boolean isLastPage = false;// 是否有前一页private boolean hasPreviousPage = false;// 是否有下一页private boolean hasNextPage = false;// 导航页码数private int navigatePages;// 所有导航页号private int[] navigatepageNums;// 导航条上的第一页private int navigateFirstPage;// 导航条上的最后一页private int navigateLastPage;
PageHelper 提供了丰富的存储和管理分页相关的参数配置选项,例如:
public PageInfo<User> getUsers(int pageNum, int pageSize, String orderBy) {PageHelper.startPage(pageNum, pageSize).setOrderBy(orderBy);List<User> userList = userMapper.selectUsers(null); return new PageInfo<>(userList);
}
PageInfo 作为后端返回给前端的标准分页数据格式,便于前端渲染分页组件。它不仅包含了分页数据,还提供了更多的辅助信息,如是否为第一页、最后一页、导航页码等,前端在使用时无需手动计算分页参数,提高可读性和可维护性。
public void print PageInfo pageInfo) {System.out.println(" 当前页码:" + pageInfo.getPageNum()); System.out.println(" 总记录数:" + pageInfo.getTotal()); System.out.println(" 总页数:" + pageInfo.getPages());
}
四、PageHelper 实现原理
4.1 使用 ThreadLocal 记录分页参数
在调用 startPage
方法时,会通过 ThreadLocal 存储当前分页参数:
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {Page<E> page = new Page<E>(pageNum, pageSize, count);page.setReasonable(reasonable);page.setPageSizeZero(pageSizeZero);//当已经执行过orderBy的时候Page<E> oldPage = getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}//设置ThreadLocalsetLocalPage(page);return page;
}
附录
分页插件参数
分页插件提供了多个可选参数,这些参数使用时,按照上面配置方式中的示例配置即可。分页插件可选参数如下表所示:
属性 | 简要说明 |
---|---|
Boolean offsetAsPageNum | 通常用于指定是否将传入的 offset 当作 pageNum 页码使用。 因为 PageHelper 默认使用页码(pageNum)、每页记录数(pageSize)来进行分页的。 |
Boolean rowBoundsWithCount | 用于指定是否进行 count 查询以获取总记录数。 设置为 true 表示 PageHelper 在执行分页查询时,会先执行一个 count 查询来获取总记录数。 |
Boolean pageSizeZero | |
Boolean reasonable | |
Boolean supportMethodsArguments | |
String dialect | 不同数据库 SQL 语句不同,指定了数据库方言为 Mysql。 |
String helperDialect | 指定分页插件使用哪种方言,候选值可参考 PageAutoDialect.java。 |
Boolean autoRuntimeDialect | 设置为 true 时,允许在运行时根据多数据源自动识别对应方言 |
Boolean autoDialect | |
Boolean closeConn | 通过该属性来设置是否关闭获取的这个连接,默认 true 关闭,设置为 false 后,不会关闭获取的连接。 |
String params | |
Boolean defaultCount | 用于控制默认不带 count 查询的方法中,是否执行 count 查询,默认 true 会执行 count 查询。 |
String dialectAlias | 允许配置自定义实现的 别名,可以用于根据 JDBC URL 自动获取对应实现,允许通过此种方式覆盖已有的实现, |
String autoDialectClass | 用于自动获取数据库类型 |
String countColumn | 用于配置自动 count 查询时的查询列,默认值 0,也就是 count(0)。 |
String replaceSql | 可选值为 regex 和 simple,默认值空时采用 regex 方式 |
String sqlCacheClass | 针对 SQLServer 生成的 count 和 page sql 进行缓存 |
String boundSqlInterceptors | |
Boolean keepOrderBy | 转换count查询时保留查询的 order by 排序。 |
Boolean keepSubSelectOrderBy | 转换count查询时保留子查询的 order by 排序。 |
Boolean asyncCount | |
String countSqlParser | |
String orderBySqlParser | |
String sqlServerSqlParser |
常见问题及解决方法
分页上下文未清理导致干扰
在同一个查询操作中,如果多次调用 PageHelper.startPage()
方法,可能会导致分页参数被覆盖或产生不可预期的结果。
/*** 第一次分页查询*/
PageHelper.startPage(1, 10);
List<DataA> listA = mapperA.queryDataA();/*** 第二次分页查询*/
PageHelper.startPage(2, 5);
List<DataB> listB = mapperB.queryDataB();// 结果可能被第二次分页干扰
PageInfo<DataA> pageInfoA = new PageInfo<>(listA);
对此,在每次分页查询后,调用 PageHelper.clearPage()
清理上下文。
PageHelper.startPage(1, 10);
List<DataA> listA = mapperA.queryDataA();
PageInfo<DataA> pageInfoA = new PageInfo<>(listA);
// 清理上下文
PageHelper.clearPage();PageHelper.startPage(2, 5);
List<DataB> listB = mapperB.queryDataB();
PageInfo<DataB> pageInfoB = new PageInfo<>(listB);
PageHelper.clearPage();
查询未执行导致上下文污染
如果分页查询代码在条件分支中,而分支未被执行,分页上下文未被清理会干扰后续查询。
PageHelper.startPage(1, 10);
if (someCondition) {List<Data> list = mapper.queryData(); // 查询未执行
}
// 后续查询会被干扰
对此,可以将分页查询代码移到条件分支内部,确保分页逻辑与查询一一对应。或者在条件分支中调用 PageHelper.clearPage()
清理上下文。
总结
通过本文的学习,你已经掌握了 PageHelper 的核心概念、使用方法和实际应用。从它的起源到现代应用,再到具体的代码实现和最佳实践,每一个环节都进行了详细的讲解。未来,随着 MyBatis 和 PageHelper 的不断发展,分页查询的功能会越来越强大。通过正确理解和使用 PageHelper,开发者可以高效完成分页需求,同时规避潜在问题,提升系统性能与稳定性,写出更加高效、优雅的代码!