定義查詢方法

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

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

  • 使用手動定義的查詢。

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

查詢查詢策略

以下策略可用於儲存庫基礎設施來解析查詢。透過 XML 配置,您可以透過 query-lookup-strategy 屬性在名稱空間中配置策略。對於 Java 配置,您可以使用 EnableCouchbaseRepositories 註解的 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)的支援。支援的運算子可能因資料儲存而異,因此請查閱參考文件的相應部分。

  • 方法解析器支援為單個屬性(例如,findByLastnameIgnoreCase(...))或支援忽略大小寫型別的屬性(通常是 String 例項,例如,findByLastnameAndFirstnameAllIgnoreCase(...))設定 IgnoreCase 標誌。是否支援忽略大小寫可能因儲存而異,因此請查閱參考文件中特定於儲存的查詢方法的相關部分。

  • 您可以透過在查詢方法後附加 OrderBy 子句並引用屬性以及提供排序方向(AscDesc)來應用靜態排序。要建立支援動態排序的查詢方法,請參閱“分頁、迭代大型結果集、排序和限制”。

保留方法名稱

雖然派生儲存庫方法按名稱繫結到屬性,但在涉及從目標識別符號屬性的基礎儲存庫繼承的某些方法名稱時,此規則有一些例外。這些保留方法,如 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 基儲存庫方法。因此,它不是一個派生查詢,使用 id 作為屬性名稱會產生誤導,因為它是一個保留方法
4 它按名稱針對 pk 屬性,因為它是一個派生查詢。
5 它透過使用 findby 之間的描述性標記來針對 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 的 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 的常用模式。通常,這些型別透過呼叫返回集合類型別的儲存庫方法並手動建立包裝器型別的例項來使用。您可以避免額外的步驟,因為 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 塊,如下例所示
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.getOffset() 處,Pageable.getPageSize() + 1

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

Slice 只能導航到下一個 Slice

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

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

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”個最大元素的查詢方法。
© . This site is unofficial and not affiliated with VMware.