滾動

滾動(Scrolling)是一種更細粒度的方法,用於迭代大型結果集塊。滾動由穩定排序、滾動型別(基於偏移量或基於 Keyset 的滾動)和結果限制組成。你可以透過使用屬性名稱定義簡單的排序表示式,並透過查詢派生使用 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 物件允許你按請求定義動態限制,而不是應用靜態限制。在查詢方法詳情中閱讀更多關於動態排序和限制的資訊。

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
}

使用偏移量進行滾動

偏移量滾動(Offset scrolling)類似於分頁,使用一個偏移量計數器跳過一定數量的結果,並讓資料來源只返回從給定偏移量開始的結果。這種簡單的機制避免了將大型結果傳送到客戶端應用程式。然而,大多數資料庫在你的伺服器返回結果之前,需要物化完整的查詢結果。

示例 1. 在倉庫查詢方法中使用 OffsetScrollPosition
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 的元素。

ScollPosition.offset()ScollPosition.offset(0L) 之間是有區別的。前者表示滾動操作的開始,不指向任何特定的偏移量,而後者標識結果中的第一個元素(位於位置 0)。鑑於滾動的排他性,使用 ScollPosition.offset(0) 會跳過第一個元素,並轉換為偏移量 1

使用 Keyset 過濾進行滾動

基於偏移量的方法要求大多數資料庫在你的伺服器返回結果之前,物化整個結果。因此,雖然客戶端只看到請求結果的一部分,但你的伺服器需要構建完整結果,這會造成額外負載。

Keyset 過濾透過利用資料庫的內建能力來處理結果子集檢索,旨在減少單個查詢的計算和 I/O 要求。這種方法維護一組鍵,透過將鍵傳遞到查詢中來恢復滾動,從而有效地修改你的過濾條件。

Keyset 過濾的核心思想是使用穩定的排序順序開始檢索結果。一旦你想滾動到下一個塊,你會獲得一個 ScrollPosition,用於在排序結果中重構位置。ScrollPosition 捕獲當前 Window 中最後一個實體的 Keyset。為了執行查詢,重構會重寫條件子句,以包含所有排序欄位和主鍵,從而使資料庫能夠利用潛在的索引來執行查詢。資料庫只需要從給定的 Keyset 位置構建一個更小的結果,而無需完全物化一個大型結果,然後再跳過結果直到達到特定偏移量。

Keyset 過濾要求 Keyset 屬性(用於排序的那些屬性)是非可空的。此限制是由於儲存特定的比較運算子的 null 值處理以及需要針對索引源執行查詢而產生的。對可空屬性進行 Keyset 過濾會導致意外結果。

在倉庫查詢方法中使用 KeysetScrollPosition
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 從頭開始,不應用額外過濾。

Keyset 過濾在資料庫包含與排序欄位匹配的索引時效果最佳,因此靜態排序效果很好。應用 Keyset 過濾的滾動查詢要求查詢返回用於排序順序的屬性,並且這些屬性必須對映到返回的實體中。

你可以使用介面和 DTO 投影,但請確保包含所有用於排序的屬性,以避免 Keyset 提取失敗。

指定 Sort 順序時,包含與查詢相關的排序屬性就足夠了;如果你不想,無需確保查詢結果是唯一的。Keyset 查詢機制透過包含主鍵(或複合主鍵的任何其餘部分)來修正你的排序順序,以確保每個查詢結果都是唯一的。