使用 Spring Data JPA 迭代大型結果集的模式
一、概述
在本教程中,我們將探索通過 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
添加一個接收Pableable
作為參數並返回Slice
的方法:
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
Slice<Student> findAllByFirstName(String firstName, Pageable page);
}
我們可以注意到返回類型是Slice<Students>
。 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);
因此,我們將一個一個地處理實體,而不是同時將它們全部加載到內存中。但是,我們需要使用 try-with-resource 塊手動關閉 Spring Data JPA 創建的流。此外,我們必須將查詢包裝在只讀事務中。
最後,即使我們一一處理行,我們也必須確保持久性上下文不會保留對所有實體的引用。我們可以通過在使用流之前手動分離實體來實現這一點:
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 上找到代碼示例。