定義查詢方法

Repository 代理有兩種方式從方法名派生特定儲存的查詢

  • 直接從方法名派生查詢。

  • 使用手動定義的查詢。

可用選項取決於實際的儲存。但是,必須有一種策略來決定建立實際的查詢。下一節描述了可用選項。

查詢查詢策略

repository 基礎設施可以使用以下策略來解析查詢。使用 XML 配置時,可以透過 query-lookup-strategy 屬性在名稱空間級別配置策略。對於 Java 配置,可以使用 EnableJpaRepositories 註解的 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…By, exists…By)定義了查詢的主題,第二部分構成了謂詞。引入子句(主題)可以包含進一步的表示式。find(或其他引入關鍵詞)和 By 之間的任何文字都被認為是描述性的,除非使用了結果限制關鍵詞,例如 Distinct 用於在要建立的查詢上設定 distinct 標誌,或 Top/First 用於限制查詢結果

附錄包含了查詢方法主題關鍵詞的完整列表以及查詢方法謂詞關鍵詞,包括排序和大小寫修飾符。然而,第一個 By 作為分隔符,指示實際條件謂詞的開始。在非常基本的層面上,你可以定義實體屬性上的條件,並使用 AndOr 連線它們。

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

  • 表示式通常是屬性遍歷與可以連線的運算子的組合。你可以使用 ANDOR 組合屬性表示式。屬性表示式還支援 Between, LessThan, GreaterThanLike 等運算子。支援的運算子可能因資料儲存而異,因此請查閱參考文件中的相應部分。

  • 方法解析器支援為單個屬性設定 IgnoreCase 標誌(例如,findByLastnameIgnoreCase(…)),或者為支援忽略大小寫的型別的所有屬性設定(通常是 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 之間的描述性標記來指向 id 屬性,以避免與保留方法衝突。

這種特殊行為不僅針對查詢方法,也適用於 existsdelete 方法。請參考“Repository 查詢關鍵詞”檢視方法列表。

屬性表示式

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

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

返回集合或 Iterable 的 Repository 方法

返回多個結果的查詢方法可以使用標準的 Java IterableListSet。除此之外,我們還支援返回 Spring Data 的 Streamable(它是 Iterable 的自定義擴充套件)以及 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。通常,這些型別是透過呼叫返回集合型別的方法並手動建立包裝器型別例項來使用的。你可以避免這個額外的步驟,因為 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 ListSeq,一個 java.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 塊,如下面的示例所示
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.getOffset() 位置的 Pageable.getPageSize() + 1

Pageable.getOffset() 開始,應用限制,執行一到多個查詢來獲取資料。

Slice 只能導航到下一個 Slice

  • Slice 提供了關於是否還有更多資料可獲取的詳細資訊。

  • 當偏移量過大時,基於偏移量的查詢會變得低效,因為資料庫仍然必須實現完整的結果集。

  • Window 提供了關於是否還有更多資料可獲取的詳細資訊。

  • 當偏移量過大時,基於偏移量的查詢會變得低效,因為資料庫仍然必須實現完整的結果集。

Page<T>

Pageable.getOffset() 位置的 Pageable.getPageSize()

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(String lastname, Limit limit);

User findFirstByOrderByLastnameAsc();

User findTopByLastnameOrderByAgeDesc(String lastname);

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

Slice<User> findTop3By(Pageable pageable);

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

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

限制表示式還支援 Distinct 關鍵詞,適用於支援 distinct 查詢的資料儲存。此外,對於將結果集限制為一個例項的查詢,支援使用 Optional 關鍵詞包裝結果。

如果對限制性查詢應用了分頁或切片(以及可用頁數的計算),則其應用範圍在受限結果集內。

將限制結果集與使用 Sort 引數進行的動態排序相結合,你可以表達查詢方法來獲取 'K' 個最小元素以及 'K' 個最大元素。