概述
在这个教程中,我们将探讨使用 Spring Data JPA 获取大型数据集的各种遍历方法。
首先,我们将使用分页查询,并了解Slice
和Page
之间的区别。接着,我们将学习如何从数据库流式处理数据,而无需一次性加载所有数据。
2. 分页查询
在这种情况下,常见的做法是使用分页查询。要做到这一点,我们需要定义批量大小并执行多次查询。这样,我们就能以较小的批次处理所有实体,避免在内存中加载大量数据。
2.1. 使用切片进行分页
本文中的代码示例将使用Student
实体作为数据模型:
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String firstName;
private String lastName;
// consturctor, getters and setters
}
让我们添加一个方法,通过firstName
查询所有学生。在Spring Data JPA中,我们只需在JpaRepository
中添加一个接收Pageable
参数并返回Slice
的方法:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
Slice<Student> findAllByFirstName(String firstName, Pageable page);
}
我们可以注意到,返回类型是Slice<Student>
。Slice
对象允许我们处理第一个批次的Student
实体。slice
对象提供了hasNext()
方法,用于检查我们正在处理的批次是否是结果集的最后一个。
此外,我们可以借助nextPageable()
方法从一个切片移动到下一个切片。这个方法返回请求下一个切片所需的Pageable
对象。因此,我们可以通过在一个while
循环中结合这两个方法,逐块获取所有数据:
void processStudentsByFirstName(String firstName) {
Slice<Student> slice = repository.findAllByFirstName(firstName, PageRequest.of(0, BATCH_SIZE));
List<Student> studentsInBatch = slice.getContent();
studentsInBatch.forEach(emailService::sendEmailToStudent);
while(slice.hasNext()) {
slice = repository.findAllByFirstName(firstName, slice.nextPageable());
slice.get().forEach(emailService::sendEmailToStudent);
}
}
让我们使用一个小批量大小运行一个简短的测试,并跟踪SQL语句。我们期望执行多次查询:
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.first_name=? limit ?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.first_name=? limit ? offset ?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.first_name=? limit ? offset ?
2.2. 使用页面进行分页
作为Slice<>
的替代,我们也可以将查询的返回类型设置为Page<>
:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
Slice<Student> findAllByFirstName(String firstName, Pageable page);
Page<Student> findAllByLastName(String lastName, Pageable page);
}
Page
接口继承了Slice
,并向其添加了两个额外的方法:getTotalPages()
和getTotalElements()
。
通常,当通过网络请求分页数据时,我们会使用Page<>
作为返回类型。这样,调用者就能确切地知道还有多少行以及需要多少次额外请求。
另一方面,使用Page<>
会引发额外的查询来统计符合标准的行数:
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.last_name=? limit ?
[main] DEBUG org.hibernate.SQL - select count(student0_.id) as col_0_0_ from student student0_ where student0_.last_name=?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.last_name=? limit ? offset ?
[main] DEBUG org.hibernate.SQL - select count(student0_.id) as col_0_0_ from student student0_ where student0_.last_name=?
[main] DEBUG org.hibernate.SQL - select student0_.id as id1_0_, student0_.first_name as first_na2_0_, student0_.last_name as last_nam3_0_ from student student0_ where student0_.last_name=? limit ? offset ?
因此,只有在需要知道实体总数的情况下,才应该使用Page<>
作为返回类型。
3. 从数据库流式处理
Spring Data JPA还允许我们从结果集中流式处理数据:
Stream<Student> findAllByFirstName(String firstName);
这样,我们就可以逐个处理实体,而不会一次性将它们全部加载到内存中。然而,我们需要手动关闭由Spring Data JPA创建的流,使用try-with-resources
块。此外,我们需要将查询封装在只读事务中。
最后,即使我们逐行处理,也要确保持久性上下文没有保留所有实体的引用。我们可以在消费流之前手动解耦这些实体:
private final EntityManager entityManager;
@Transactional(readOnly = true)
public void processStudentsByFirstNameUsingStreams(String firstName) {
try (Stream<Student> students = repository.findAllByFirstName(firstName)) {
students.peek(entityManager::detach)
.forEach(emailService::sendEmailToStudent);
}
}
4. 总结
在这篇文章中,我们探索了处理大型数据集的不同方法。起初,我们通过分页查询实现这一目标。我们了解到,当调用者需要知道总元素数时,应使用Page<>
,否则使用Slice<>
。接下来,我们学习了如何从数据库流式处理数据并逐行处理。
如往常一样,代码示例可在GitHub上找到。