定義查詢方法

儲存庫代理有兩種方式可從方法名稱衍生出特定於儲存區的查詢

  • 直接從方法名稱衍生查詢。

  • 使用手動定義的查詢。

可用的選項取決於實際的儲存區。但是,必須有一種策略來決定要建立的實際查詢。下一節將說明可用的選項。

查詢查找策略

以下策略可用於儲存庫基礎架構以解析查詢。使用 XML 組態,您可以使用 query-lookup-strategy 屬性在命名空間中組態策略。對於 Java 組態,您可以使用 EnableMongoRepositories 注釋的 queryLookupStrategy 屬性。某些策略可能不支援特定的資料儲存。

  • CREATE 嘗試從查詢方法名稱建構特定於儲存區的查詢。一般方法是從方法名稱中移除一組已知的字首,並剖析方法的其餘部分。您可以在「查詢建立」中閱讀有關查詢建構的更多資訊。

  • USE_DECLARED_QUERY 嘗試尋找宣告的查詢,如果找不到,則擲回例外。查詢可以通過某處的注釋來定義,或通過其他方式宣告。請參閱特定儲存區的文件,以尋找該儲存區的可用選項。如果儲存庫基礎架構在啟動時找不到方法的宣告查詢,則會失敗。

  • CREATE_IF_NOT_FOUND (預設值) 結合了 CREATEUSE_DECLARED_QUERY。它首先查找宣告的查詢,如果找不到宣告的查詢,則建立自訂的基於方法名稱的查詢。這是預設的查找策略,因此,如果您未明確組態任何內容,則會使用它。它允許通過方法名稱快速定義查詢,並且還可以通過在需要時引入宣告的查詢來對這些查詢進行自訂調整。

查詢建立

Spring Data 儲存庫基礎架構中內建的查詢建立器機制,對於在儲存庫的實體上建立約束查詢非常有用。

以下範例顯示如何建立多個查詢

從方法名稱建立查詢
interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

剖析查詢方法名稱分為主詞和述詞。第一部分 (find…Byexists…By) 定義查詢的主詞,第二部分構成述詞。引入子句 (主詞) 可以包含更多運算式。find (或其他引入關鍵字) 和 By 之間的任何文字都被視為描述性的,除非使用結果限制關鍵字之一,例如 Distinct 以在要建立的查詢上設定 distinct 旗標,或 Top/First 以限制查詢結果

附錄包含 查詢方法主詞關鍵字的完整列表查詢方法述詞關鍵字,包括排序和字母大小寫修飾符。但是,第一個 By 作為分隔符,指示實際條件述詞的開始。在非常基礎的層級,您可以在實體屬性上定義條件,並使用 AndOr 將它們串連起來。

剖析方法的實際結果取決於您建立查詢的持久性儲存區。但是,有一些一般事項需要注意

  • 運算式通常是屬性遍歷,並與可以串連的運算符結合使用。您可以將屬性運算式與 ANDOR 結合使用。您還可以獲得對屬性運算式的運算符支援,例如 BetweenLessThanGreaterThanLike。支援的運算符可能因資料儲存而異,因此請查閱參考文件的相應部分。

  • 方法剖析器支援為個別屬性設定 IgnoreCase 旗標 (例如,findByLastnameIgnoreCase(…)),或為支援忽略大小寫的類型的所有屬性設定 IgnoreCase 旗標 (通常為 String 實例 — 例如,findByLastnameAndFirstnameAllIgnoreCase(…))。是否支援忽略大小寫可能因儲存區而異,因此請查閱參考文件中特定於儲存區的查詢方法中的相關章節。

  • 您可以通過將 OrderBy 子句附加到引用屬性的查詢方法,並提供排序方向 (AscDesc) 來應用靜態排序。要建立支援動態排序的查詢方法,請參閱「分頁、迭代大型結果、排序與限制」。

保留方法名稱

雖然衍生的儲存庫方法通過名稱繫結到屬性,但當涉及到從以識別碼屬性為目標的基本儲存庫繼承的某些方法名稱時,此規則有一些例外。這些保留方法,例如 CrudRepository#findById (或僅 findById),以識別碼屬性為目標,而與宣告的方法中使用的實際屬性名稱無關。

考慮以下網域類型,它持有一個標記為識別碼的 pk 屬性 (通過 @Id) 和一個名為 id 的屬性。在這種情況下,您需要密切注意查找方法的命名,因為它們可能會與預定義的簽名衝突

class User {
  @Id Long pk;                          (1)

  Long id;                              (2)

  // …
}

interface UserRepository extends Repository<User, Long> {

  Optional<User> findById(Long id);     (3)

  Optional<User> findByPk(Long pk);     (4)

  Optional<User> findUserById(Long id); (5)
}
1 識別碼屬性 (主鍵)。
2 名為 id 的屬性,但不是識別碼。
3 pk 屬性為目標 (標記有 @Id 的屬性,它被視為識別碼),因為它引用了 CrudRepository 基本儲存庫方法。因此,它不是使用 id 的衍生查詢,因為屬性名稱會暗示它是保留方法之一。
4 通過名稱以 pk 屬性為目標,因為它是衍生查詢。
5 通過使用 findby 之間的描述性 token 以 id 屬性為目標,以避免與保留方法衝突。

這種特殊的行為不僅針對查找方法,也適用於 exitsdelete 方法。請參閱「儲存庫查詢關鍵字」以獲取方法列表。

屬性運算式

屬性運算式只能引用受管理實體的直接屬性,如前面的範例所示。在查詢建立時,您已經確保剖析的屬性是受管理網域類別的屬性。但是,您也可以通過遍歷巢狀屬性來定義約束。考慮以下方法簽名

List<Person> findByAddressZipCode(ZipCode zipCode);

假設 Person 具有帶有 ZipCodeAddress。在這種情況下,該方法會建立 x.address.zipCode 屬性遍歷。解析演算法首先將整個部分 (AddressZipCode) 解釋為屬性,並檢查網域類別是否具有該名稱的屬性 (未大寫)。如果演算法成功,則使用該屬性。如果沒有,演算法會從右側將來源在駝峰式部分分割為頭部和尾部,並嘗試尋找對應的屬性 — 在我們的範例中,為 AddressZipCode。如果演算法找到具有該頭部的屬性,則採用尾部並從那裡繼續向下建構樹狀結構,以剛才描述的方式分割尾部。如果第一次分割不匹配,則演算法將分割點向左移動 (AddressZipCode) 並繼續。

儘管這應該適用於大多數情況,但演算法有可能選擇錯誤的屬性。假設 Person 類別也具有 addressZip 屬性。演算法將在第一輪分割中已經匹配,選擇錯誤的屬性並失敗 (因為 addressZip 的類型可能沒有 code 屬性)。

為了消除這種歧義,您可以在方法名稱中使用 _ 來手動定義遍歷點。因此,我們的方法名稱將如下所示

List<Person> findByAddress_ZipCode(ZipCode zipCode);

由於我們將底線 (_) 視為保留字元,因此我們強烈建議遵循標準 Java 命名慣例 (即,不在屬性名稱中使用底線,而是應用駝峰式命名)。

以底線開頭的欄位名稱

欄位名稱可能以底線開頭,例如 String _name。確保保留 _,如 _name 中所示,並使用雙底線 __ 來分割巢狀路徑,例如 user__name

大寫欄位名稱

全部為大寫的欄位名稱可以按原樣使用。巢狀路徑 (如果適用) 需要通過 _ 分割,如 USER_name 中所示。

第二個字母為大寫的欄位名稱

由以小寫字母開頭,後跟大寫字母組成的欄位名稱 (例如 String qCode) 可以通過以兩個大寫字母開頭來解析,如 QCode 中所示。請注意潛在的路徑歧義。

路徑歧義

在以下範例中,屬性 qCodeq 的排列,其中 q 包含名為 code 的屬性,為路徑 QCode 建立了歧義。

record Container(String qCode, Code q) {}
record Code(String code) {}

由於首先考慮直接匹配屬性,因此不會考慮任何潛在的巢狀路徑,並且演算法會選擇 qCode 欄位。為了選擇 q 中的 code 欄位,需要底線表示法 Q_Code

儲存庫方法傳回集合或可迭代物件

傳回多個結果的查詢方法可以使用標準 Java IterableListSet。除此之外,我們還支援傳回 Spring Data 的 Streamable (Iterable 的自訂擴充功能) 以及 Vavr 提供的集合類型。請參閱附錄,其中說明了所有可能的 查詢方法傳回類型

使用 Streamable 作為查詢方法傳回類型

您可以將 Streamable 用作 Iterable 或任何集合類型的替代方案。它提供了方便的方法來存取非並行的 Stream (Iterable 中缺少) 以及直接在元素上進行 ….filter(…)….map(…) 以及將 Streamable 連接到其他 Streamable 的能力

使用 Streamable 組合查詢方法結果
interface PersonRepository extends Repository<Person, Long> {
  Streamable<Person> findByFirstnameContaining(String firstname);
  Streamable<Person> findByLastnameContaining(String lastname);
}

Streamable<Person> result = repository.findByFirstnameContaining("av")
  .and(repository.findByLastnameContaining("ea"));

傳回自訂 Streamable 包裝函式類型

為集合提供專用的包裝函式類型是常用模式,用於為傳回多個元素的查詢結果提供 API。通常,這些類型通過調用傳回類似集合類型的儲存庫方法並手動建立包裝函式類型的實例來使用。您可以避免該額外步驟,因為如果這些包裝函式類型符合以下條件,Spring Data 允許您將它們用作查詢方法傳回類型

  1. 類型實作 Streamable

  2. 類型公開建構函式或名為 of(…)valueOf(…) 的靜態工廠方法,該方法將 Streamable 作為引數。

以下清單顯示了一個範例

class Product {                                         (1)
  MonetaryAmount getPrice() { … }
}

@RequiredArgsConstructor(staticName = "of")
class Products implements Streamable<Product> {         (2)

  private final Streamable<Product> streamable;

  public MonetaryAmount getTotal() {                    (3)
    return streamable.stream()
      .map(Product::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }


  @Override
  public Iterator<Product> iterator() {                 (4)
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository<Product, Long> {
  Products findAllByDescriptionContaining(String text); (5)
}
1 一個 Product 實體,它公開了存取產品價格的 API。
2 Streamable<Product> 的包裝函式類型,可以使用 Products.of(…) 建構 (使用 Lombok 注釋建立的工廠方法)。採用 Streamable<Product> 的標準建構函式也同樣適用。
3 包裝函式類型公開了一個額外的 API,用於計算 Streamable<Product> 上的新值。
4 實作 Streamable 介面並委派給實際結果。
5 該包裝函式類型 Products 可以直接用作查詢方法傳回類型。您無需傳回 Streamable<Product> 並在儲存庫用戶端中的查詢後手動包裝它。

支援 Vavr 集合

Vavr 是一個在 Java 中採用函數式程式設計概念的程式庫。它附帶了一組自訂集合類型,您可以將其用作查詢方法傳回類型,如下表所示

Vavr 集合類型 使用的 Vavr 實作類型 有效的 Java 來源類型

io.vavr.collection.Seq

io.vavr.collection.List

java.util.Iterable

io.vavr.collection.Set

io.vavr.collection.LinkedHashSet

java.util.Iterable

io.vavr.collection.Map

io.vavr.collection.LinkedHashMap

java.util.Map

您可以使用第一列中的類型 (或其子類型) 作為查詢方法傳回類型,並根據實際查詢結果的 Java 類型 (第三列) 取得第二列中用作實作類型的類型。或者,您可以宣告 Traversable (Vavr Iterable 等效項),然後我們從實際傳回值衍生實作類別。也就是說,java.util.List 變為 Vavr ListSeqjava.util.Set 變為 Vavr LinkedHashSet Set,依此類推。

串流查詢結果

您可以使用 Java 8 Stream<T> 作為傳回類型,以遞增方式處理查詢方法的結果。可以使用資料儲存區特定的方法來執行串流,而不是將查詢結果包裝在 Stream 中,如下列範例所示

使用 Java 8 Stream<T> 串流查詢結果
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
Stream 可能會包裝底層的資料儲存區特定資源,因此必須在使用後關閉。您可以通過使用 close() 方法手動關閉 Stream,或通過使用 Java 7 try-with-resources 區塊 (如下列範例所示) 來關閉 Stream
try-with-resources 區塊中使用 Stream<T> 結果
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}
並非所有 Spring Data 模組目前都支援 Stream<T> 作為傳回類型。

非同步查詢結果

您可以使用 Spring 的非同步方法執行功能,以非同步方式執行儲存庫查詢。這表示方法在調用後立即傳回,而實際查詢發生在已提交到 Spring TaskExecutor 的任務中。非同步查詢與反應式查詢不同,不應混用。有關反應式支援的更多詳細資訊,請參閱特定於儲存區的文件。以下範例顯示了多個非同步查詢

@Async
Future<User> findByFirstname(String firstname);               (1)

@Async
CompletableFuture<User> findOneByFirstname(String firstname); (2)
1 使用 java.util.concurrent.Future 作為傳回類型。
2 使用 Java 8 java.util.concurrent.CompletableFuture 作為傳回類型。

分頁、迭代大型結果、排序與限制

若要處理查詢中的參數,請定義方法參數,如前面的範例中已見。除此之外,基礎架構會識別某些特定類型,例如 PageableSortLimit,以將分頁、排序和限制動態應用於您的查詢。以下範例示範了這些功能

在查詢方法中使用 PageableSliceSortLimit
Page<User> findByLastname(String lastname, Pageable pageable);

Slice<User> findByLastname(String lastname, Pageable pageable);

List<User> findByLastname(String lastname, Sort sort);

List<User> findByLastname(String lastname, Sort sort, Limit limit);

List<User> findByLastname(String lastname, Pageable pageable);
採用 SortPageableLimit 的 API 預期將非 null 值傳遞到方法中。如果您不想應用任何排序或分頁,請使用 Sort.unsorted()Pageable.unpaged()Limit.unlimited()

第一種方法允許您將 org.springframework.data.domain.Pageable 實例傳遞到查詢方法,以將分頁動態新增到靜態定義的查詢。Page 知道可用元素的總數和頁數。它通過基礎架構觸發計數查詢來計算總數。由於這可能會很耗費資源 (取決於使用的儲存區),您可以改為傳回 SliceSlice 僅知道是否還有下一個 Slice 可用,這在遍歷較大的結果集時可能就足夠了。

排序選項也通過 Pageable 實例處理。如果您只需要排序,請將 org.springframework.data.domain.Sort 參數新增到您的方法。如您所見,傳回 List 也是可能的。在這種情況下,不會建立建構實際 Page 實例所需的其他中繼資料 (反過來說,這表示不會發出本來必要的額外計數查詢)。相反,它將查詢限制為僅查找給定範圍的實體。

若要找出整個查詢有多少頁,您必須觸發額外的計數查詢。預設情況下,此查詢是從您實際觸發的查詢衍生而來。

特殊參數只能在查詢方法中使用一次。
上面描述的某些特殊參數是互斥的。請考慮以下無效參數組合的列表。

參數 範例 原因

PageableSort

findBy…​(Pageable page, Sort sort)

Pageable 已經定義了 Sort

PageableLimit

findBy…​(Pageable page, Limit limit)

Pageable 已經定義了限制。

用於限制結果的 Top 關鍵字可以與 Pageable 一起使用,其中 Top 定義了結果的總最大值,而 Pageable 參數可能會減少此數字。

哪種方法合適?

Spring Data 抽象提供的價值或許最好通過下表概述的可能查詢方法傳回類型來展示。下表顯示您可以從查詢方法傳回哪些類型

表 1. 使用大型查詢結果
方法 擷取的資料量 查詢結構 約束

List<T>

所有結果。

單一查詢。

查詢結果可能會耗盡所有記憶體。擷取所有資料可能非常耗時。

Streamable<T>

所有結果。

單一查詢。

查詢結果可能會耗盡所有記憶體。擷取所有資料可能非常耗時。

Stream<T>

分塊 (逐個或分批),取決於 Stream 使用情況。

通常使用游標的單一查詢。

使用後必須關閉 Stream,以避免資源洩漏。

Flux<T>

分塊 (逐個或分批),取決於 Flux 使用情況。

通常使用游標的單一查詢。

儲存模組必須提供反應式基礎架構。

Slice<T>

Pageable.getPageSize() + 1 位於 Pageable.getOffset()

一個到多個查詢,擷取從 Pageable.getOffset() 開始的資料,並應用限制。

Slice 只能導航到下一個 Slice

  • Slice 提供有關是否有更多資料要擷取的詳細資訊。

  • 當偏移量太大時,基於偏移量的查詢會變得效率低下,因為資料庫仍然必須具體化完整結果。

  • Window 提供有關是否有更多資料要擷取的詳細資訊。

  • 當偏移量太大時,基於偏移量的查詢會變得效率低下,因為資料庫仍然必須具體化完整結果。

Page<T>

Pageable.getPageSize() 位於 Pageable.getOffset()

一個到多個查詢,從 Pageable.getOffset() 開始,應用限制。此外,可能需要 COUNT(…) 查詢來確定元素的總數。

通常,需要 COUNT(…) 查詢,這些查詢成本很高。

  • 當偏移量太大時,基於偏移量的查詢會變得效率低下,因為資料庫仍然必須具體化完整結果。

分頁和排序

您可以使用屬性名稱定義簡單的排序運算式。您可以串連運算式以將多個條件收集到一個運算式中。

定義排序運算式
Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

為了更類型安全地定義排序運算式,請從要定義排序運算式的類型開始,並使用方法參考來定義要排序的屬性。

使用類型安全 API 定義排序運算式
TypedSort<Person> person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());
TypedSort.by(…) 利用執行時期代理 (通常) 通過使用 CGlib,當使用 Graal VM Native 等工具時,這可能會干擾原生映像檔編譯。

如果您的儲存區實作支援 Querydsl,您也可以使用產生的中繼模型類型來定義排序運算式

使用 Querydsl API 定義排序運算式
QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

限制查詢結果

除了分頁之外,還可以通過使用專用的 Limit 參數來限制結果大小。您還可以通過使用 FirstTop 關鍵字來限制查詢方法的結果,您可以互換使用這些關鍵字,但不可以與 Limit 參數混合使用。您可以將可選的數值附加到 TopFirst,以指定要傳回的最大結果大小。如果省略數字,則假定結果大小為 1。以下範例顯示如何限制查詢大小

使用 TopFirst 限制查詢的結果大小
List<User> findByLastname(Limit limit);

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

限制運算式也支援 Distinct 關鍵字,適用於支援 distinct 查詢的資料儲存區。此外,對於將結果集限制為單個實例的查詢,支援使用 Optional 關鍵字包裝結果。

如果將分頁或切片應用於限制查詢分頁 (以及可用頁數的計算),則將其應用於限制的結果內。

通過使用 Sort 參數將結果限制與動態排序結合使用,您可以表達用於「K」個最小元素以及「K」個最大元素的查詢方法。