滾動 (Scrolling)

滾動 (Scrolling) 是一種更細緻的方法,用於迭代處理較大結果集區塊。滾動包含穩定的排序 (stable sort)、滾動類型 (scroll type)(基於偏移量 (Offset) 或 Keyset 的滾動)和結果限制 (result limiting)。您可以使用屬性名稱來定義簡單的排序表示式,並透過查詢衍生使用 TopFirst 關鍵字來定義靜態結果限制。您可以串連表示式以將多個條件收集到一個表示式中。

滾動查詢會回傳 Window<T>,讓您能夠取得元素的滾動位置,以提取下一個 Window<T>,直到您的應用程式使用完整個查詢結果。類似於透過取得下一批結果來使用 Java Iterator<List<…>>,查詢結果滾動可讓您透過 Window.positionAt(…​) 存取 ScrollPosition

Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", ScrollPosition.offset());
do {

  for (User u : users) {
    // consume the user
  }

  // obtain the next Scroll
  users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && users.hasNext());

ScrollPosition 識別元素在整個查詢結果中的確切位置。查詢執行將位置參數視為排除性,結果將在給定位置之後開始。ScrollPosition#offset()ScrollPosition#keyset() 作為 ScrollPosition 的特殊化身,指示滾動操作的開始。

上面的範例顯示了靜態排序和限制。您可以選擇定義接受 Sort 物件的查詢方法,以定義更複雜的排序順序或基於每個請求的排序。以類似的方式,提供 Limit 物件可讓您基於每個請求定義動態限制,而不是套用靜態限制。請在查詢方法詳細資訊 (Query Methods Details)中閱讀更多關於動態排序和限制的資訊。

WindowIterator 提供了一個實用工具,透過移除檢查下一個 Window 是否存在以及套用 ScrollPosition 的需求,來簡化跨 Window 的滾動。

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.offset());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}

使用偏移量滾動 (Scrolling using Offset)

偏移量滾動 (Offset scrolling) 使用類似於分頁 (pagination) 的方式,使用偏移量計數器 (Offset counter) 來跳過一些結果,並讓資料來源僅回傳從給定偏移量開始的結果。這種簡單的機制避免了將大量結果傳送到用戶端應用程式。然而,大多數資料庫都需要實體化完整的查詢結果,伺服器才能回傳結果。

範例 1. 將 OffsetScrollPosition 與 Repository 查詢方法一起使用 (Using OffsetScrollPosition with Repository Query Methods)
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(OffsetScrollPosition.initial()); (1)
1 從沒有偏移量開始,以包含位置 0 的元素。(Start with no offset to include the element at position 0.)

ScollPosition.offset()ScollPosition.offset(0L) 之間存在差異。前者表示滾動操作的開始,指向非特定偏移量,而後者識別結果的第一個元素(位置 0)。鑑於滾動的排除性性質,使用 ScollPosition.offset(0) 會跳過第一個元素並轉換為偏移量 1。(There is a difference between ScollPosition.offset() and ScollPosition.offset(0L). The former indicates the start of scroll operation, pointing to no specific offset whereas the latter identifies the first element (at position 0) of the result. Given the exclusive nature of scrolling, using ScollPosition.offset(0) skips the first element and translate to an offset of 1.)

使用 Keyset 過濾滾動 (Scrolling using Keyset-Filtering)

基於偏移量的方法 (Offset-based) 需要大多數資料庫在伺服器回傳結果之前實體化整個結果。因此,雖然用戶端僅看到請求結果的一部分,但您的伺服器需要建立完整結果,這會導致額外的負載。(Offset-based requires most databases require materializing the entire result before your server can return the results. So while the client only sees the portion of the requested results, your server needs to build the full result, which causes additional load.)

Keyset 過濾 (Keyset-Filtering) 方法透過利用資料庫的內建功能來處理結果子集檢索,旨在減少個別查詢的計算和 I/O 需求。此方法維護一組金鑰,透過將金鑰傳遞到查詢中來恢復滾動,有效地修改您的篩選條件。(Keyset-Filtering approaches result subset retrieval by leveraging built-in capabilities of your database aiming to reduce the computation and I/O requirements for individual queries. This approach maintains a set of keys to resume scrolling by passing keys into the query, effectively amending your filter criteria.)

Keyset 過濾的核心概念是開始使用穩定的排序順序檢索結果。一旦您想要滾動到下一個區塊,您將獲得一個 ScrollPosition,該位置用於重建排序結果內的位置。ScrollPosition 捕獲目前 Window 內最後一個實體的 keyset。為了執行查詢,重建會重寫條件子句,以包含所有排序欄位和主鍵,以便資料庫可以利用潛在的索引來執行查詢。資料庫只需要從給定的 keyset 位置建構小得多的結果,而無需完全實體化大型結果,然後跳過結果直到達到特定偏移量。(The core idea of Keyset-Filtering is to start retrieving results using a stable sorting order. Once you want to scroll to the next chunk, you obtain a ScrollPosition that is used to reconstruct the position within the sorted result. The ScrollPosition captures the keyset of the last entity within the current Window. To run the query, reconstruction rewrites the criteria clause to include all sort fields and the primary key so that the database can leverage potential indexes to run the query. The database needs only constructing a much smaller result from the given keyset position without the need to fully materialize a large result and then skipping results until reaching a particular offset.)

Keyset 過濾要求 keyset 屬性(用於排序的屬性)為不可為 null (non-nullable)。此限制適用於商店特定 (store specific) 的比較運算子 null 值處理,以及針對索引來源執行查詢的需求。對可為 null (nullable) 屬性進行 Keyset 過濾將導致意外的結果。(Keyset-Filtering requires the keyset properties (those used for sorting) to be non-nullable. This limitation applies due to the store specific null value handling of comparison operators as well as the need to run queries against an indexed source. Keyset-Filtering on nullable properties will lead to unexpected results.)

KeysetScrollPosition 與 Repository 查詢方法一起使用 (Using KeysetScrollPosition with Repository Query Methods)
interface UserRepository extends Repository<User, Long> {

  Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}

WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
  .startingAt(ScrollPosition.keyset()); (1)
1 從最開始處開始,並且不套用額外的篩選。(Start at the very beginning and do not apply additional filtering.)

當您的資料庫包含與排序欄位匹配的索引時,Keyset 過濾效果最佳,因此靜態排序效果良好。應用 Keyset 過濾的滾動查詢要求查詢回傳排序順序中使用的屬性,並且這些屬性必須映射在回傳的實體中。(Keyset-Filtering works best when your database contains an index that matches the sort fields, hence a static sort works well. Scroll queries applying Keyset-Filtering require to the properties used in the sort order to be returned by the query, and these must be mapped in the returned entity.)

您可以使用介面和 DTO 投影,但請確保包含您已排序的所有屬性,以避免 keyset 提取失敗。(You can use interface and DTO projections, however make sure to include all properties that you’ve sorted by to avoid keyset extraction failures.)

當指定您的 Sort 順序時,包含與您的查詢相關的排序屬性就足夠了;如果您不想,則無需確保唯一的查詢結果。keyset 查詢機制透過包含主鍵(或複合主鍵的任何剩餘部分)來修改您的排序順序,以確保每個查詢結果都是唯一的。(When specifying your Sort order, it is sufficient to include sort properties relevant to your query; You do not need to ensure unique query results if you do not want to. The keyset query mechanism amends your sort order by including the primary key (or any remainder of composite primary keys) to ensure each query result is unique.)