定義查詢方法
Repository 代理有兩種方法可以從方法名稱派生出針對特定儲存的查詢:
-
直接從方法名稱派生查詢。
-
使用手動定義的查詢。
可用選項取決於實際的儲存。但是,必須有一個策略來決定建立什麼實際的查詢。下一節描述了可用的選項。
查詢查詢策略
以下策略可用於 Repository 基礎設施解析查詢。透過 XML 配置,你可以透過 query-lookup-strategy
屬性在名稱空間上配置策略。對於 Java 配置,你可以使用 EnableJpaRepositories
註解的 queryLookupStrategy
屬性。某些策略可能不支援特定的資料儲存。
-
CREATE
嘗試從查詢方法名稱構造針對特定儲存的查詢。一般方法是移除方法名稱中一組已知的公共字首,並解析方法的其餘部分。你可以在“查詢建立”中閱讀更多關於查詢構造的內容。 -
USE_DECLARED_QUERY
嘗試查詢已宣告的查詢,如果找不到則丟擲異常。查詢可以透過註解或其他方式在某處定義。請查閱特定儲存的文件以瞭解該儲存的可用選項。如果在啟動時 Repository 基礎設施找不到該方法的已宣告查詢,則會失敗。 -
CREATE_IF_NOT_FOUND
(預設)結合了CREATE
和USE_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);
}
解析查詢方法名稱分為主體 (subject) 和謂詞 (predicate)。第一部分(如 find…By
, exists…By
)定義查詢的主體,第二部分構成謂詞。引入子句(主體)可以包含進一步的表示式。find
(或其他引入關鍵字)和 By
之間的任何文字都被認為是描述性的,除非使用了限制結果的關鍵字,例如 Distinct
用於為要建立的查詢設定 distinct 標誌,或者Top
/First
用於限制查詢結果。
附錄包含了完整的查詢方法主體關鍵字列表以及查詢方法謂詞關鍵字,包括排序和字母大小寫修飾符。然而,第一個 By
充當分隔符,指示實際條件謂詞的開始。在非常基本的層面上,你可以定義實體屬性的條件,並使用 And
和 Or
將它們連線起來。
解析方法的實際結果取決於你建立查詢所針對的持久化儲存。但是,有一些通用的注意事項:
-
表示式通常是屬性遍歷與可連線的運算子相結合。你可以使用
AND
和OR
組合屬性表示式。屬性表示式還支援諸如Between
,LessThan
,GreaterThan
和Like
等運算子。支援的運算子可能因資料儲存而異,因此請查閱你的參考文件中相應的部分。 -
方法解析器支援為單個屬性設定
IgnoreCase
標誌(例如findByLastnameIgnoreCase(…)
),或為支援忽略大小寫的所有型別屬性設定此標誌(通常是String
例項——例如findByLastnameAndFirstnameAllIgnoreCase(…)
)。是否支援忽略大小寫可能因儲存而異,因此請查閱參考文件中關於特定儲存查詢方法的有關章節。 -
你可以透過向引用屬性的查詢方法附加
OrderBy
子句並提供排序方向(Asc
或Desc
)來應用靜態排序。要建立支援動態排序的查詢方法,請參閱“分頁、迭代大結果集、排序與限制”。
保留的方法名稱
雖然派生的 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 | 它透過使用 find 和 by 之間的描述性標記來指向 id 屬性,以避免與 保留方法 衝突。 |
這種特殊行為不僅適用於查詢方法,也適用於 exists
和 delete
方法。請參閱“Repository 查詢關鍵字”以獲取方法列表。
屬性表示式
屬性表示式只能引用託管實體的直接屬性,如前面的示例所示。在查詢建立時,你已經確保解析的屬性是託管域類的屬性。但是,你也可以透過遍歷巢狀屬性來定義約束。考慮以下方法簽名:
List<Person> findByAddressZipCode(ZipCode zipCode);
假設一個 Person
擁有一個帶有 ZipCode
的 Address
。在這種情況下,該方法會建立 x.address.zipCode
屬性遍歷。解析演算法首先將整個部分(AddressZipCode
)解釋為屬性,並在域類中檢查是否存在該名稱(首字母小寫)的屬性。如果演算法成功,則使用該屬性。如果失敗,演算法會從右側的駝峰命名部分將源拆分為頭部和尾部,並嘗試查詢相應的屬性 — 在我們的示例中是 AddressZip
和 Code
。如果演算法找到了帶有該頭部的屬性,它會取尾部並從那裡繼續向下構建樹,以剛描述的方式拆分尾部。如果第一次拆分不匹配,演算法會將拆分點向左移動(Address
,ZipCode
)並繼續。
儘管這在大多數情況下應該可行,但演算法有可能選擇錯誤的屬性。假設 Person
類也包含一個 addressZip
屬性。演算法會在第一輪拆分中就匹配到,選擇錯誤的屬性,並失敗(因為 addressZip
的型別可能沒有 code
屬性)。
為了解決這種歧義,你可以在方法名稱中使用 _
手動定義遍歷點。因此,我們的方法名稱將如下所示:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因為我們將下劃線( |
以下劃線開頭的欄位名稱
欄位名稱可以以下劃線開頭,例如 全大寫欄位名稱
全大寫的欄位名稱可以照常使用。如果適用,巢狀路徑需要透過 第二個字母大寫的欄位名稱
欄位名稱由一個起始小寫字母后跟一個大寫字母組成,例如 路徑歧義
在以下示例中,屬性
由於首先考慮對屬性進行直接匹配,任何潛在的巢狀路徑都不會被考慮,演算法會選擇 |
返回集合或可迭代物件的 Repository 方法
返回多個結果的查詢方法可以使用標準的 Java Iterable
, List
和 Set
。此外,我們還支援返回 Spring Data 的 Streamable
(Iterable
的自定義擴充套件)以及 Vavr 提供的集合型別。請參考附錄中解釋所有可能的查詢方法返回型別的部分。
使用 Streamable 作為查詢方法的返回型別
你可以使用 Streamable
作為 Iterable
或任何集合型別的替代品。它提供了方便的方法來訪問非並行 Stream
(這是 Iterable
所缺少的),並且可以直接對元素進行 ….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 允許你將這些包裝型別用作查詢方法的返回型別,前提是它們滿足以下條件:
-
型別實現了
Streamable
介面。 -
型別提供了一個建構函式或名為
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 | 一個暴露了訪問產品價格 API 的 Product 實體。 |
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 源型別 |
---|---|---|
|
|
|
|
|
|
|
|
|
你可以將第一列中的型別(或其子型別)用作查詢方法的返回型別,並根據實際查詢結果的 Java 型別(第三列)獲得第二列中用作實現型別的型別。或者,你可以宣告 Traversable
(Vavr 的 Iterable
等效項),然後我們將從實際返回值中派生出實現類。也就是說,一個 java.util.List
會轉換為 Vavr 的 List
或 Seq
,一個 java.util.Set
會變成 Vavr 的 LinkedHashSet
Set
等等。
流式查詢結果
你可以透過使用 Java 8 Stream<T>
作為返回型別來增量處理查詢方法的結果。與將查詢結果包裝在 Stream
中不同,這裡使用針對特定資料儲存的方法來執行流式處理,如下例所示:
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 作為返回型別。 |
分頁、迭代大結果集、排序與限制
要處理查詢中的引數,請定義方法引數,如前例所示。此外,基礎設施會識別某些特定型別,如 Pageable
, Sort
和 Limit
,以便動態地為你的查詢應用分頁、排序和限制。以下示例展示了這些功能:
Pageable
, Slice
, Sort
和 Limit
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 , Pageable 和 Limit 的 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
例項所需的額外元資料不會被建立(反過來,這意味著本來必要的額外計數查詢不會被髮出)。相反,它將查詢限制為僅查詢給定範圍的實體。
要確定整個查詢有多少頁,你需要觸發額外的計數查詢。預設情況下,此查詢是根據你實際觸發的查詢派生而來的。 |
特殊引數在查詢方法中只能使用一次。
用於限制結果的關鍵字 |
哪種方法是合適的?
Spring Data 抽象提供的值或許最好地體現在下表中列出的可能的查詢方法返回型別中。該表展示了你可以從查詢方法返回的型別:
方法 | 獲取的資料量 | 查詢結構 | 約束 |
---|---|---|---|
所有結果。 |
單個查詢。 |
查詢結果可能會耗盡所有記憶體。獲取所有資料可能非常耗時。 |
|
所有結果。 |
單個查詢。 |
查詢結果可能會耗盡所有記憶體。獲取所有資料可能非常耗時。 |
|
分塊處理(逐個或批次),取決於 |
通常使用遊標的單個查詢。 |
使用後必須關閉 Stream 以避免資源洩漏。 |
|
|
分塊處理(逐個或批次),取決於 |
通常使用遊標的單個查詢。 |
儲存模組必須提供響應式基礎設施。 |
|
在 |
從 |
一個
|
|
在 |
從 |
通常需要成本高昂的
|
分頁和排序
你可以透過使用屬性名稱來定義簡單的排序表示式。你可以連線表示式將多個條件收集到一個表示式中。
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
為了更型別安全地定義排序表示式,請從要定義排序表示式的型別開始,並使用方法引用來定義要排序的屬性。
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,你也可以使用生成的元模型型別來定義排序表示式:
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
限制查詢結果
除了分頁,還可以使用專用的 Limit
引數來限制結果大小。你還可以透過使用 First
或 Top
關鍵字來限制查詢方法的結果,這兩個關鍵字可以互換使用,但不能與 Limit
引數混用。你可以在 Top
或 First
後附加一個可選的數字值,以指定要返回的最大結果大小。如果省略該數字,則假定結果大小為 1。以下示例展示瞭如何限制查詢大小:
Top
和 First
限制查詢結果大小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' 個最大元素的方法。 |