概述

在这个教程中,我们将探讨使用 Spring Data JPA 获取大型数据集的各种遍历方法

首先,我们将使用分页查询,并了解SlicePage之间的区别。接着,我们将学习如何从数据库流式处理数据,而无需一次性加载所有数据。

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上找到。