定義查詢方法

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

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

  • 使用手動定義的查詢。

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

查詢查找策略

以下策略可用於儲存庫基礎架構來解析查詢。使用 XML 設定時,您可以透過命名空間中的 query-lookup-strategy 屬性來設定策略。對於 Java 設定,您可以使用 EnableJpaRepositories 註解的 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…By, exists…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 之間的描述性標記以 id 屬性為目標,以避免與保留方法衝突。

此特殊行為不僅針對查找方法,也適用於 exitsdelete 方法。有關方法清單,請參閱「儲存庫查詢關鍵字」。

屬性表達式

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

List<Person> findByAddressZipCode(ZipCode zipCode);

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

雖然這應該適用於大多數情況,但演算法可能會選取錯誤的屬性。假設 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 的 StreamableIterable 的自訂擴充功能)以及 Vavr 提供的集合類型。請參閱附錄,其中說明了所有可能的 查詢方法傳回類型

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

您可以使用 Streamable 作為 Iterable 或任何集合類型的替代方案。它提供了便利的方法來存取非並行的 StreamIterable 中缺少),以及直接對元素執行 ….filter(…)….map(…) 的能力,並將 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() 方法或使用 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 的取用。

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

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

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 關鍵字,適用於支援相異查詢的資料儲存區。此外,對於將結果集限制為一個實例的查詢,支援使用 Optional 關鍵字包裝結果。

如果將分頁或切片套用至限制查詢分頁(以及可用頁數的計算),則會在受限制的結果內套用。

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