定義查詢方法

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 來設定查詢的去重標誌,或者 Top/First 來限制查詢結果

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

解析方法的實際結果取決於你建立查詢所使用的持久化儲存。然而,有一些需要注意的通用事項

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

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

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

保留方法名

雖然派生的 repository 方法按名稱繫結到屬性,但對於某些從基礎 repository 繼承的方法名,如果它們的目標是 identifier 屬性,則存在一些例外。那些保留方法,例如 CrudRepository#findById(或僅 findById),無論在宣告的方法中使用的實際屬性名是什麼,它們都指向 identifier 屬性。

考慮以下域型別,它包含一個透過 @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

返回集合或可迭代物件的 Repository 方法

返回多個結果的查詢方法可以使用標準的 Java Iterable, ListSet。除此之外,我們還支援返回 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。通常,這些型別透過呼叫返回類似集合型別的 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 的 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,以動態地將分頁、排序和限制應用於你的查詢。以下示例展示了這些功能

在查詢方法中使用 Pageable, Slice, SortLimit
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);
接受 Sort, PageableLimit 的 API 期望傳遞非 null 值給方法。如果你不想應用任何排序或分頁,請使用 Sort.unsorted(), Pageable.unpaged()Limit.unlimited()

第一個方法允許你將 org.springframework.data.domain.Pageable 例項傳遞給查詢方法,以便動態地為靜態定義的查詢新增分頁。一個 Page 知道元素的總數和可用的總頁數。它透過基礎結構觸發一個計數查詢來計算總數。由於這可能很昂貴(取決於使用的儲存),你可以改為返回一個 Slice。一個 Slice 只知道是否有下一個 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 關鍵字,適用於支援去重查詢的資料儲存。此外,對於將結果集限制為一個例項的查詢,支援使用 Optional 關鍵字包裝結果。

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

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