定義查詢方法

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

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

  • 使用手動定義的查詢。

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

查詢查找策略

以下策略可用於 repository 基礎架構來解析查詢。使用 XML 設定時,您可以透過命名空間中的 query-lookup-strategy 屬性來設定策略。對於 Java 設定,您可以使用 EnableJdbcRepositories 註解的 queryLookupStrategy 屬性。某些策略可能不支援特定的資料儲存區。

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

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

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

查詢建立

Spring Data repository 基礎架構中內建的查詢建構器機制對於在 repository 的實體上建構限制性查詢非常有用。

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

從方法名稱建立查詢
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)來應用靜態排序。若要建立支援動態排序的查詢方法,請參閱「分頁、迭代大型結果、排序和限制」。

保留方法名稱

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

考慮以下網域類型,它持有一個透過 @Id 標記為識別符的屬性 pk 和一個名為 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 基本 repository 方法。因此,它不是使用 id 的衍生查詢,因為屬性名稱會建議它是保留方法之一。
4 由於它是衍生查詢,因此依名稱目標為 pk 屬性。
5 透過使用 findby 之間的描述性 token 來避免與保留方法衝突,從而目標為 id 屬性。

這種特殊行為不僅目標為查找方法,而且也適用於 exitsdelete 方法。請參閱「Repository 查詢關鍵字」以取得方法列表。

屬性表達式

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

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

Repository 方法傳回集合或 Iterable

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

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

您可以將 Streamable 用作 Iterable 或任何集合類型的替代方案。它提供了便利的方法來存取非平行 StreamIterable 中缺少),以及直接對元素進行 ….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。通常,這些類型是透過調用傳回類似集合類型的 repository 方法並手動建立包裝器類型的實例來使用的。您可以避免這個額外的步驟,因為 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>,然後在 repository 用戶端中的查詢後手動包裝它。

支援 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> 作為傳回類型,以增量方式處理查詢方法的結果。Spring Data 模組不會將查詢結果包裝在 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 的非同步方法執行功能,以非同步方式執行 repository 查詢。這表示方法在調用後立即傳回,而實際查詢發生在已提交到 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() + 1Pageable.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」個最大元素的查詢方法。