定義查詢方法
儲存庫代理有兩種方法可以從方法名稱衍生出特定於儲存區的查詢
-
直接從方法名稱衍生查詢。
-
透過使用手動定義的查詢。
可用選項取決於實際的儲存區。但是,必須有一種策略來決定要建立哪個實際的查詢。下一節將描述可用的選項。
查詢查找策略
以下策略可用於儲存庫基礎架構以解析查詢。使用 XML 配置,您可以透過 `query-lookup-strategy` 屬性在命名空間中配置策略。對於 Java 配置,您可以使用 `EnableElasticsearchRepositories` 注解的 `queryLookupStrategy` 屬性。某些策略可能不支援特定的資料儲存區。
-
`CREATE` 嘗試從查詢方法名稱建構特定於儲存區的查詢。一般方法是從方法名稱中移除一組已知的字首,並解析方法的其餘部分。您可以在「查詢建立」中閱讀更多關於查詢建構的資訊。
-
`USE_DECLARED_QUERY` 嘗試尋找已宣告的查詢,如果找不到則拋出例外。查詢可以透過某處的註解定義,或透過其他方式宣告。請參閱特定儲存區的文件以尋找該儲存區的可用選項。如果儲存庫基礎架構在啟動時找不到該方法的已宣告查詢,則會失敗。
-
`CREATE_IF_NOT_FOUND`(預設值)結合了 `CREATE` 和 `USE_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…By`、`exists…By`)定義查詢的主詞,第二部分構成述詞。引入子句(主詞)可以包含更多表達式。`find`(或其他引入關鍵字)和 `By` 之間的任何文字都被視為描述性的,除非使用結果限制關鍵字之一,例如 `Distinct` 以在要建立的查詢上設定 distinct 標誌,或 `Top`/`First` 以限制查詢結果。
附錄包含查詢方法主詞關鍵字和查詢方法述詞關鍵字的完整列表,包括排序和字母大小寫修飾符。然而,第一個 `By` 充當分隔符,指示實際條件述詞的開始。在非常基礎的層級,您可以定義實體屬性的條件,並使用 `And` 和 `Or` 將它們串聯起來。
解析方法的實際結果取決於您建立查詢的持久性儲存區。但是,有一些一般事項需要注意
-
表達式通常是屬性遍歷,與可以串聯的運算符結合使用。您可以將屬性表達式與 `AND` 和 `OR` 結合使用。您還可以獲得對屬性表達式的運算符的支持,例如 `Between`、`LessThan`、`GreaterThan` 和 `Like`。支援的運算符可能因資料儲存區而異,因此請查閱參考文件中相應的部分。
-
方法解析器支援為個別屬性(例如,`findByLastnameIgnoreCase(…)`)或所有支援忽略大小寫的類型屬性(通常是 `String` 實例 — 例如,`findByLastnameAndFirstnameAllIgnoreCase(…)`)設定 `IgnoreCase` 標誌。是否支援忽略大小寫可能因儲存區而異,因此請查閱參考文件中關於特定於儲存區的查詢方法的相關章節。
-
您可以透過將 `OrderBy` 子句附加到引用屬性的查詢方法並提供排序方向(`Asc` 或 `Desc`)來應用靜態排序。若要建立支援動態排序的查詢方法,請參閱「分頁、迭代大量結果、排序與限制」。
保留方法名稱
雖然衍生的儲存庫方法按名稱綁定到屬性,但當涉及到從基礎儲存庫繼承的某些方法名稱,這些方法名稱以識別符屬性為目標時,此規則存在一些例外。那些保留方法,例如 `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 | 透過在 `find` 和 `by` 之間使用描述性 token 以 `id` 屬性為目標,以避免與保留方法衝突。 |
這種特殊行為不僅以查找方法為目標,而且也適用於 `exists` 和 `delete` 方法。有關方法列表,請參閱「儲存庫查詢關鍵字」。
屬性表達式
如前面的範例所示,屬性表達式只能引用受管理實體的直接屬性。在查詢建立時,您已經確保解析的屬性是受管理網域類別的屬性。但是,您也可以透過遍歷巢狀屬性來定義約束。考慮以下方法簽章
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);
由於我們將底線 (`_`) 視為保留字元,因此我們強烈建議遵循標準 Java 命名慣例(也就是說,不要在屬性名稱中使用底線,而是應用駝峰式命名)。 |
以底線開頭的欄位名稱
欄位名稱可以底線開頭,例如 `String _name`。請確保保留 `_`,如 `_name` 中所示,並使用雙底線 `__` 來拆分巢狀路徑,例如 `user__name`。 大寫欄位名稱
所有大寫的欄位名稱都可以照常使用。巢狀路徑(如果適用)需要透過 `_` 拆分,如 `USER_name` 中所示。 第二個字母為大寫的欄位名稱
由小寫字母開頭,後跟大寫字母組成的欄位名稱,例如 `String qCode`,可以透過以兩個大寫字母開頭來解析,例如 `QCode`。請注意潛在的路徑歧義。 路徑歧義
在以下範例中,屬性 `qCode` 和 `q` 的排列,其中 `q` 包含一個名為 `code` 的屬性,會為路徑 `QCode` 建立歧義。
由於首先考慮直接匹配屬性,因此不會考慮任何潛在的巢狀路徑,並且演算法會選擇 `qCode` 欄位。為了選擇 `q` 中的 `code` 欄位,需要底線表示法 `Q_Code`。 |
傳回集合或可迭代物件的儲存庫方法
傳回多個結果的查詢方法可以使用標準 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 | 一個 `Product` 實體,它公開 API 以存取產品的價格。 |
2 | 一個 `Streamable |
3 | 包裝器類型公開了一個額外的 API,用於計算 `Streamable |
4 | 實作 `Streamable` 介面並委派給實際結果。 |
5 | 該包裝器類型 `Products` 可以直接用作查詢方法回傳類型。您不需要傳回 `Streamable |
對 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
@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 (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
並非所有 Spring Data 模組目前都支援 `Stream |
非同步查詢結果
您可以使用 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` 作為回傳類型。 |
分頁、迭代大量結果、排序與限制
若要處理查詢中的參數,請定義方法參數,如前面的範例所示。除此之外,基礎架構還識別某些特定類型,例如 `Pageable`、`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` 實例所需的額外元數據(這反過來意味著不會發出本來必要的額外計數查詢)。相反,它將查詢限制為僅查找給定範圍的實體。
若要找出整個查詢有多少頁,您必須觸發額外的計數查詢。預設情況下,此查詢是從您實際觸發的查詢衍生而來的。 |
特殊參數在一個查詢方法中只能使用一次。
用於限制結果的 `Top` 關鍵字可以與 `Pageable` 一起使用,其中 `Top` 定義結果的總最大值,而 Pageable 參數可能會減少此數量。 |
哪種方法合適?
Spring Data 抽象提供的價值或許最好透過以下表格中概述的可能查詢方法回傳類型來展示。下表顯示您可以從查詢方法傳回哪些類型
方法 | 提取的資料量 | 查詢結構 | 約束 |
---|---|---|---|
所有結果。 |
單一查詢。 |
查詢結果可能會耗盡所有記憶體。提取所有資料可能非常耗時。 |
|
所有結果。 |
單一查詢。 |
查詢結果可能會耗盡所有記憶體。提取所有資料可能非常耗時。 |
|
分塊(逐個或分批),取決於 `Stream` 的消耗。 |
單一查詢,通常使用游標。 |
串流必須在使用後關閉,以避免資源洩漏。 |
|
|
分塊(逐個或分批),取決於 `Flux` 的消耗。 |
單一查詢,通常使用游標。 |
儲存模組必須提供反應式基礎架構。 |
|
`Pageable.getPageSize() + 1`,位於 `Pageable.getOffset()` |
一對多查詢,從 `Pageable.getOffset()` 開始提取資料並應用限制。 |
`Slice` 只能導航到下一個 `Slice`。
|
|
`Pageable.getPageSize()`,位於 `Pageable.getOffset()` |
一對多查詢,從 `Pageable.getOffset()` 開始提取資料並應用限制。此外,可能需要 `COUNT(…)` 查詢來確定元素總數。 |
通常,需要 `COUNT(…)` 查詢,這些查詢成本很高。
|
分頁和排序
您可以使用屬性名稱定義簡單的排序表達式。您可以串聯表達式以將多個條件收集到一個表達式中。
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。以下範例顯示如何限制查詢大小
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' 個最大元素的查詢方法。 |