Spring Data JPA 分页接口的使用方法——PageHelper分页查询方法

作者:禅与计算机程序设计艺术

1.简介

2017年,全国IT企业纷纷转向云计算、容器技术,并掀起了一股“容器潮流”。容器化、微服务架构以及分布式系统架构模式越来越流行,对于大型复杂系统而言,数据库分页查询在性能上就成为一个重要的优化点。基于此,当下互联网中最流行的开源框架之一, MyBatis 和 SpringData JPA 等都提供了分页查询功能。
PageHelper 是 MyBatis 中的一个分页插件,作用是在不用改变 SQL 查询的前提下,实现自定义物理分页,非常方便开发者使用。由于 MyBatis 本身对 JDBC 的支持较弱,所以 PageHelper 可以帮助 MyBatis 在查询时自动进行物理分页。通过设置合理的分页参数,只需要简单地添加 PageHelper 作为 MyBatis 的插件即可实现分页查询,不需要编写复杂的 SQL 或多余的代码。
此外,Spring Data JPA 提供了自己的分页接口(PagingAndSortingRepository)用于解决分页查询问题。该接口继承自JpaSpecificationExecutor接口,可以满足各种复杂的分页查询需求。Spring Data JPA 会将分页请求翻译成相应的 SQL 语句,然后再根据指定的分页参数返回结果集。
本文将详细介绍 PageHelper 及其 Spring Data JPA 分页接口的使用方法。

2.背景介绍

什么是分页查询?

简单的说,分页查询就是为了降低数据量对内存、CPU的消耗,提高数据库的查询效率和响应速度,减少网络传输带宽等资源的开销,在一定范围内取出目标数据的一部分,以便于显示给用户。分页查询往往分为两种:物理分页和逻辑分页。

  1. 物理分页
    物理分页就是直接限制页面显示的数据条目数量,比如每页显示 10 条数据,这样虽然可以加快数据的呈现速度,但会导致数据不连续、无法精确查找,一般仅适用于很少的记录或者结构比较简单的表格。
  2. 逻辑分页
    逻辑分页指根据用户设定的条件过滤数据,再按照一定的规则排序后,按指定大小切割数据,最后输出指定页面的数据。逻辑分页不要求每次查询都要返回相同数量的数据,而且可以实现更细致的检索。例如,在搜索结果页面,用户可以按相关性、时间、价格等条件筛选数据,然后进行排序,最后显示符合条件的前几页数据。

为什么要分页查询?

当数据量过大时,使用物理分页会占用大量的内存,并且影响数据库服务器的性能。相反,使用逻辑分页可以在不违背用户体验的情况下,避免数据量过大导致的系统卡顿或崩溃。此外,使用逻辑分页还可以有效地防止用户恶意攻击,抵御一些基于爬虫的恶意攻击,从而保护网站和数据安全。

3.基本概念术语说明

一、物理分页

物理分页是指在数据库层面限制页面显示的数据条目数量,它通过指定 LIMIT offset,page_size 来实现。

  • OFFSET: 表示偏移量,表示从哪一条记录开始读取,默认为 0。
  • PAGE_SIZE: 表示每页显示多少条记录,即 SELECT COUNT(*) FROM table LIMIT page_size OFFSET offset; 。

二、逻辑分页

逻辑分页是指按照用户设定的条件过滤数据,再按照一定的规则排序后,按照指定大小切割数据,输出指定页面的数据。

1. 什么是过滤器?

搜索引擎把一个大的网页分成许多小的网页,然后每个小的网页各有一个独立的 URL。当用户输入关键词搜索某些网站时,搜索引擎会首先抓取所有包含关键词的网页,然后将这些网页组装成一个整体,提供给用户查看。这个过程称为索引。但是这种抓取索引的方式效率不高,尤其当网页很多时。因此搜索引擎一般会采用过滤器机制。
滤波器是指在搜索结果中,排除掉不符合用户要求的内容。用户可选择设置多个过滤器,如“所有网站”、“百度网盘”、“腾讯视频”,过滤器可以对搜索结果进行初步的筛查,去除掉那些不感兴趣的内容。

2. 什么是排序?

在搜索结果中,搜索引擎一般都会对搜索结果进行排序。搜索结果排序通常有两种方式:

  • 根据相关性排序:相关性计算的是文档之间的相关程度,比如两个文档之间共同出现的单词数量、主题等。
  • 根据评价排序:评价指的是文档的好坏程度,比如文档的发布日期、质量等。
    用户也可以设置多个排序方式,如按相关性排序、按发布日期排序等。

3. 切分数据?

数据是存在磁盘中的,不能全部加载到内存里处理。因此,当搜索结果超过一定数量时,需要对数据进行切分。搜索引擎一般是按照页码来切分数据的。用户输入关键字、选择过滤器、选择排序方式等,搜索引擎会首先计算出符合要求的文档的总数量,然后将总数量划分成若干个固定大小的页面。用户点击第 n 个页面时,搜索引擎会首先计算出它对应的文档的位置,然后把文档内容读入内存进行展示。

四、分页组件

1. PageHelper

PageHelper 是 MyBatis 的一个分页插件,它可以很方便地为 MyBatis 的执行器增加物理分页功能。分页插件利用 MyBatis 对象关系映射工具动态的拦截StatementHandler,在SQL执行之前,拼接分页查询的SQL语句,然后通过ResultSet Handler 对结果进行拼装。因此,无需修改应用层代码,即可实现物理分页功能。

2. Spring Data JPA 分页接口 PagingAndSortingRepository

Spring Data JPA 提供了一个名叫 PagingAndSortingRepository 的分页接口,该接口继承自 JpaSpecificationExecutor,该接口可以方便的实现分页查询,支持以下查询方法:

List<T> findAll(Sort sort); // 返回全部元素,排序方式由sort决定
List<T> findAll(Pageable pageable); // 返回全部元素,分页信息由pageable决定
Page<T> findAll(Specification<T> spec, Pageable pageable); // 按照条件查询,分页信息由pageable决定

通过该接口可以获取全部元素列表,分页查询,分页查询和条件查询。Pageable 对象封装了分页信息,包括页码、页面大小、排序字段、排序方向等。Sort 对象封装了排序信息,包括排序字段及排序方向等。Specification 对象封装了查询条件,支持丰富的查询条件组合。

4.核心算法原理和具体操作步骤以及数学公式讲解

PageHelper 分页查询接口实际上只是拦截 StatementHandler 对象,对执行 SQL 后的 ResultSet 对象进行包装,以达到分页的目的。具体来说,分页插件拦截到的 StatementHandler 对象有两种情况:

  • 如果没有任何条件,那么生成的 SQL 语句就是普通的 SELECT * FROM user; ,这时 MyBatis 会默认给查询结果设置 limit offset,page_size。
  • 如果有任何条件,那么生成的 SQL 语句类似于 SELECT * FROM user WHERE id=?; ,这时 MyBatis 只会返回一条记录,因此也不会产生分页效果。
    在 MyBatis 中,配置分页的关键是设置 标签下的 com.github.pagehelper.PageInterceptor。具体的分页参数如下:
<plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"><property name="dialect" value=""/><property name="helperDialect" value="${jdbc.type}"/><property name="reasonable" value="false"/><property name="pageSizeZero" value="true"/><property name="countSqlSupport" value="true"/><property name="rowBoundsWithCount" value="true"/><property name="supportMethodsArguments" value="true"/><property name="paramsMap" value="false"/>plugin>
plugins>

从配置文件可以看出,分页插件的主要参数有:

  • dialect:设置数据库类型,比如 MySQL、Oracle等。
  • helperDialect:设置自动识别数据库类型。
  • reasonable:是否启用合理化。合理化指的是根据你的数据库里的数据量、查询次数、用户请求等因素,自动调整分页参数。比如,当查询第 1 页数据时,它只会查询前 10 条数据;当查询第 2 页数据时,它只会查询 10-20 条数据;当查询第 n 页数据时,它只会查询 (n-1) * pageSize 条数据。你可以通过 reasonable 属性开启合理化功能。
  • pageSizeZero:是否允许查询 pageSize = 0 的数据。比如,设置为 false 时,如果 pageSize=0,则 MyBatis 会抛出异常。
  • rowBoundsWithCount:是否启用 count 查询。如果设置为 true,MyBatis 会在查询语句的末尾追加 SELECT COUNT(*) FROM yourTable,然后查询时只返回第一行的值,并且在 ResultSet 中不会放置任何数据,这会加快分页查询的速度。
  • supportMethodsArguments:是否支持方法参数。如果设置为 true,那么在你的 Mapper 方法里,你可以通过 @Param("username") String username 这样的参数来传递分页参数。
  • paramsMap:是否传入参数 Map 对象。如果设置为 true,那么 Mybatis 会把请求参数放在参数对象里一起传递给 StatementHandler 对象。
    在 StatementHandler 拦截的方法中,分页插件会自动判断是否需要分页,如果需要的话,它会先根据当前页码和页容量计算出 limit 和 offset 值,然后重新生成新的 SQL 语句,其中包括了 limit 和 offset 的参数。然后,MyBatis 会生成一个新的 PreparedStatement 对象,并执行这个新的 SQL 语句。当查询完成后,分页插件会根据查询的结果集合大小和页容量,计算出总页数。ResultSetHandler 将结果对象转换成 Page 对象,并返回给调用者。
    下面的流程描述了 PageHelper 分页查询的主要逻辑:
  1. 用户发送分页请求。
  2. DispatcherServlet 拦截请求,调用 HandlerMapping 根据请求路径找到对应的 Controller。
  3. Controller 执行分页查询,调用 Service。
  4. Service 使用 PageHelper 的分页方法对 DAO 层的数据源进行分页查询。
  5. PageHelper 根据 DAO 层的配置,以及分页请求参数,对 ResultSetHandler 的 ResultSet 对象进行包装,并返回分页结果 Page。
  6. ModelAndView 将分页结果渲染到 View 中。

5.具体代码实例和解释说明

PageHelper 实现分页查询

准备工作:引入 PageHelper 依赖
pom 文件中引入以下依赖:

<dependency><groupId>org.mybatisgroupId><artifactId>mybatisartifactId><version>${mybatis.version}version>
dependency>
<dependency><groupId>org.mybatisgroupId><artifactId>mybatis-springartifactId><version>${mybatis-spring.version}version>
dependency>
<dependency><groupId>com.github.pagehelpergroupId><artifactId>pagehelper-spring-boot-starterartifactId><version>${pagehelper.version}version>
dependency>

配置分页插件:application.yml 文件中加入以下配置:

spring:datasource:url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
pagehelper:dialect: mysqlhelperDialect: mysqlreasonable: falsepageSizeZero: truerowBoundsWithCount: truesupportMethodsArguments: trueparamsMap: true

定义实体类:

public class User {private Integer id;private String userName;private int age;// getter and setter methods...
}

创建 MyBatis mapper XML 文件:


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.dao.UserDao"><select id="selectAll" resultType="User">select * from user order by id descselect><select id="selectByCondition" parameterType="map" resultType="User">select * from user where ${key} #{value} order by id descselect>
mapper>

服务层实现分页查询:

@Service
public class UserService implements BaseService<User> {@Autowiredprivate UserMapper userMapper;/*** 分页查询*/public PageInfo<User> findListByPage(@RequestParam(required = false, defaultValue = "") String key,@RequestParam(required = false, defaultValue = "0") int pageNum,@RequestParam(required = false, defaultValue = "10") int pageSize) {// 构造分页对象PageHelper.startPage(pageNum, pageSize);if (!"".equals(key)) {HashMap<String, Object> map = new HashMap<>();map.put("key", key + " like ");map.put("value", "%");List<User> list = userMapper.selectByCondition(map);return new PageInfo<>(list);} else {List<User> list = userMapper.selectAll();return new PageInfo<>(list);}}
}

控制层测试分页查询:

@RestController
@RequestMapping("/api")
public class DemoController {@Autowiredprivate UserService userService;@GetMapping("/users/{page}/{size}")public ResponseEntity<?> findUsersList(@PathVariable int page,@PathVariable int size) {try {PageInfo<User> usersPageInfo = userService.findListByPage("", page, size);return ResponseEntity.ok().body(usersPageInfo);} catch (Exception e) {log.error("Error:", e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}
}

运行项目,访问 http://localhost:8080/api/users/1/10 ,即可看到分页查询的结果。

Spring Data JPA 实现分页查询

准备工作:引入 Spring Boot Starter Data JPA 依赖
pom 文件中引入以下依赖:

org.springframework.bootspring-boot-starter-data-jpa

配置数据源:application.yml 文件中加入以下配置:

spring:jpa:database: mysqlshow-sql: truegenerate-ddl: truehibernate:ddl-auto: updateproperties:hibernate.hbm2ddl.import_files: classpath:schema.sql
datasource:url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: root

定义实体类:

@Entity
public class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long userId;private String userName;private int age;// getter and setter methods...
}

创建 Spring Data JPA Repository 接口:

public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {}

服务层实现分页查询:

@Service
public class UserService {@Autowiredprivate UserRepository userRepository;/*** 分页查询*/public Page<User> findListByPage(@RequestParam(required = false, defaultValue = "") String key,@RequestParam(required = false, defaultValue = "0") int pageNum,@RequestParam(required = false, defaultValue = "10") int pageSize) {// 构造分页对象Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by("userId").descending());if (!"".equals(key)) {Specification specification = Specification.where(User_.userName.like("%" + key + "%"));Page<User> usersPage = userRepository.findAll(specification, pageable);return usersPage;} else {Page<User> usersPage = userRepository.findAll(pageable);return usersPage;}}
}

控制层测试分页查询:

@RestController
@RequestMapping("/api")
public class DemoController {@Autowiredprivate UserService userService;@GetMapping("/users/{page}/{size}")public ResponseEntity<?> findUsersList(@PathVariable int page,@PathVariable int size) {try {Page<User> usersPage = userService.findListByPage("", page, size);return ResponseEntity.ok().body(usersPage.getContent());} catch (Exception e) {log.error("Error:", e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}
}

运行项目,访问 http://localhost:8080/api/users/1/10 ,即可看到分页查询的结果。

6.未来发展趋势与挑战

PageHelper 及 Spring Data JPA 分页接口目前已经足够用于生产环境,大规模系统的分页查询可以使用它们解决。然而,随着业务快速发展,数据规模可能会持续增长,这时候需要更强大的分页功能。
有些分页查询的场景下,例如聚合函数、子查询、复杂排序,仍然无法使用 PageHelper 及 Spring Data JPA 分页接口解决。这时候,我们可以考虑另一种分页方案,比如 ElasticSearch。ElasticSearch 是开源的搜索引擎,支持复杂的聚合、搜索、排序、分页等功能,它能够处理海量数据的高速搜索,同时它有易于使用的 RESTful API,可以用来代替传统的分页查询接口。ElasticSearch 可以用于替代数据库的分页查询功能,不过需要自己搭建 ElasticSearch 集群、处理复杂的 SQL 语句。
更进一步,PageHelper 及 Spring Data JPA 分页接口的局限性也需要考虑。比如,分页参数的灵活性较差,不够灵活的 SQL 查询无法使用分页插件,分页逻辑处理需要自己实现,当数据规模增长时,查询效率可能变得较慢。

7.附录常见问题与解答

1.为什么 MyBatis 不支持分页?

MyBatis 原生不支持分页,因为 MyBatis 是直接对数据库连接的,而对数据库连接的操作本身就不是纯粹的 SQL 操作,而是涉及到 JDBC 规范、数据库方言、数据库类型等众多因素,很难保证分页的正确性。相比之下,Hibernate 支持更加完善,它的查询对象内部已经实现了分页逻辑,因此 MyBatis 在分页方面的能力有限。

2.如何判断 MyBatis 是否已经支持分页?

可以尝试在 MyBatis 配置文件中加入如下配置:

<setting name="useGeneratedKeys" value="true" />
<settings>

生成主键的开关。如果能够正常执行分页查询,就可以确定 MyBatis 对分页的支持。

3.PageHelper 支持 Oracle 数据库吗?

PageHelper 当前版本暂不支持 Oracle 数据库,因为 PageHelper 需要根据不同的数据库执行不同的分页策略。计划在后续的版本中支持更多数据库。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部