Spring JPA 定義查詢方法

Spring JPA 定義查詢方法

翻譯:Defining Query Methods

​ 存儲庫代理有兩種方式基於方法名派生特定域的查詢方式:

  • 直接從方法名派生查詢
  • 自定義查詢方式

​ 可用選項基於實際存儲。但是,必須有一個策略來決定創建什麼樣的實際查詢。下一節將介紹可用的選項。

1、查詢查找策略

​ 以下策略可用於存儲庫基礎結構來解決查詢。使用XML配置,可以通過querylookup strategy屬性在名稱空間配置策略。對於Java配置,可以使用Enable${store}Repositories注釋的queryLookupStrategy屬性。但某些策略可能不支持特定的數據存儲。

  • create查詢方式嘗試從查詢方法名稱構造特定於存儲的查詢。一般是刪除從方法中刪除不用的部分,然後細化用到的部分。你可以從Query-Creation了解更多關於查詢創建的內容。
  • USE_DECLARED_QUERY嘗試查找已聲明的查詢,如果找不到則引發異常。查詢可以通過某個地方的注釋進行定義,或通過其他方式進行聲明。請參閱特定存儲庫方法的文檔,以找到該存儲庫內的可用方法。如果存儲庫基礎結構在引導時未找到方法的聲明查詢,則導致失敗。
  • CREATE_IF_NOT_FOUND(默認)結合CREATEUSE_DECLARED_QUERY的查詢。它首先查找已聲明的查詢,如果沒有找到聲明的查詢,它將創建一個基於自定義方法名的查詢。這是默認的查找策略,因此,如果未顯式配置任何內容,則使用此策略。它允許通過方法名快速定義查詢,還可以根據需要引入聲明的查詢來定製這些查詢。

2、查詢創建

​ Spring數據存儲庫基礎方法中內置的查詢生成器機制對於在存儲庫的實體上構建的約束查詢非常有用。該機制從方法中剝離前綴find…By、read…By、query…By、count…Byget…By,並開始解析其餘部分。引入子句可以包含更多的表達式,例如在要創建的查詢上設置Distinct標誌的Distinct。第一個By充當分隔符,指示實際條件的開始。您可以定義實體屬性的條件,並將它們使用andOr連接起來。以下示例演示如何創建多個查詢:

例13:從方法名創建查詢

interface PersonRepository extends Repository<Person, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // 允許去重查詢
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // 允許忽略大小寫查詢
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // 允許查詢結果進行排序
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

解析方法的實際結果取決於是基於哪個持久類實體進行的查詢創建,但是,也有一些一般性問題需要注意:

  • 表達式通常是屬性字段和運算符組合在一起進行遍歷,你可以使用AND或者OR組合屬性表達式,同時也支持Between, LessThan, GreaterThan, 和Like等運算符,支持的運算符可能因數據存儲而異,具體請參考文檔的相應部分。
  • 方法解析器支持為單個屬性(例如findByLastnameIgnoreCase(…))或支持忽略大小寫的類型的所有屬性設置IgnoreCase標誌(通常是字符串實例  ,例如findByLastnameAndFirstnameAllIgnoreCase(…))。是否支持忽略大小寫可能因存儲而異,因此請參閱參考文檔中的相關部分以了解特定於存儲的查詢方法。
  • 通過向引用屬性的查詢方法追加OrderBy子句並提供排序方向(AscDesc),可以應用靜態排序。要創建支持動態排序的查詢方法,請參閱「特殊參數處理」。

3、屬性表達式

​ 屬性表達式只能引用實體類定義的直接屬性,如上例所示,在創建查詢時,你已經確定屬性是實體類對應域中的屬性,除此之外,還可以通過嵌套屬性定義約束。

List<Person> findByAddressZipCode(ZipCode zipCode);

​ 假定一個人擁有一個帶郵政編碼的地址,在這種情況下,該方法遍歷創建屬性x.address.zipCode. 解析算法首先將整個部分(AddressZipCode)解釋為屬性,然後在域類中檢查具有該名稱(未大寫)的屬性。如果算法成功,則使用該屬性。如果不是這樣,算法會把駝峰命名部分的源代碼拆分,並嘗試在我們的示例中找到相應的屬性 AddressZipCode。如果算法找到一個帶有該頭部的屬性,它將獲取尾部並繼續從那裡構建樹,並按照剛才描述的方式將尾部拆分。如果第一個拆分不匹配,則算法將拆分點向左移動(Address、ZipCode)並繼續。

舉例說明拆分:AaBbCc

第一次拆分 AaBb / Cc 獲取屬性方式 AaBb.Cc

第二次拆分 Aa / BbCc 獲取屬性方式 Aa.BbCc

​ 儘管這在大多數情況下都是可行的,但算法仍然可能會選擇錯誤的屬性。假設Person類也有一個addressZip屬性。該算法已經在第一輪分割中匹配,選擇了錯誤的屬性,然後就會失敗(因為addressZip的類型可能沒有代碼屬性)。

​ 要解決這種歧義,可以在方法名內部手動定義遍歷點(以 – 定義遍歷點)。

List<Person> findByAddress_ZipCode(ZipCode zipCode);

因為我們將下劃線字符視為保留字符,所以我們強烈建議遵循標準的Java命名約定(即在屬性名稱中不使用下劃線,而是使用駝峰大小寫)。

4、特殊參數處理

​ 要處理查詢中的參數,請像前面示例中所看到的那樣定義方法參數。除此之外,基礎結構還識別某些特定類型,如分頁和排序,動態地對查詢應用分頁和排序。下面的示例演示了這些特性。

例14:在查詢中使用Pageable, Slice, 和 Sort

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, Pageable pageable);

採用排序和可分頁的api希望將非空值傳遞給方法。如果不想應用任何排序或分頁,可以使用Sort.unsorted()和Pageable.unpaged()。

​ 第一個方法允許您傳遞一個org.springframework.data.domain查詢方法的分頁實例,以動態地向靜態定義的查詢添加分頁。Page獲取到了可用元素和頁面的總數。它是通過基礎結構觸發計數查詢來計算總數量來實現的。因為這可能會廢算力(取決於所使用的存儲),所以可以返回一個Slice。一個片只知道下一個片是否可用,這在遍歷更大的結果集時可能就足夠了。

TIPS:出於性能優化考慮,建議使用Slice

​ 排序同樣通過Pageable實例進行處理,如果你只需要進行排序,只需要在你的方法中添加一個org.springframework.data.domain.Sort參數。如您所見,返回列表也是可能的。 在這種情況下,將不會創建構建Page實例所需的其他元數據(這意味着沒有發出必要的附加計數查詢)。相反,它將查詢限制為僅查找給定範圍的實體。

要查明整個查詢得到了多少頁,必須觸發一個額外的count查詢。默認情況下,該查詢派生自您實際觸發的查詢。

可以使用屬性名定義簡單的排序表達式。可以將表達式連接起來,將多個表達式整合到一個表達式中。

例15:定義查詢表達式

Sort sort = Sort.by("firstname").ascending()
  .and(Sort.by("lastname").descending());

要以更類型安全的方式定義排序表達式,請從定義用於的排序表達式的類型開始,並使用方法引用定義要排序的屬性

例16:使用類型安全的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等工具時,CGlib可能會干擾本機映像的編譯。

如果您的存儲實現支持Querydsl,您還可以使用生成的元模型類型來定義排序表達式。

例17: 使用Querydsl API定義排序表達式

QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));

5、查詢結果限制

查詢結果可以使用互換使用的top或者first關鍵字來進行限制,可以將一個可變的數字值附加到topfirst,以指定返回的最大結果大小。如果遺漏了這個數字,則使用默認值1。下面的示例顯示如何限制查詢大小。

例18:使用topfirst限制查詢返回結果的大小

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關鍵字。另外,對於將結果集限制為一個實例的查詢,支持使用Optional關鍵字包裝結果。

如果將分頁或切片應用於限制查詢分頁(以及計算可用頁面數量),則將其應用於有限的結果。

通過使用Sort參數來限制結果與動態排序的組合,可以表達最小和最大元素的查詢方法。

6、返回集合或迭代的存儲庫方法

​ 返回多個結果的查詢方法可以使用標準的Java Iterable, List, Set。除此之外,我們還支持返回Spring數據的Streamable, Iterable的自定義擴展,以及Vavr提供的集合類型。

例19:使用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 { 
    //產品實體公開訪問價格的API
  MonetaryAmount getPrice() { … }
}

@RequiredArgConstructor(staticName = "of")
class Products implements Streamable<Product> { 
//可通過產品構造的Streamable<Product>的包裝器類型。of()(通過Lombok注釋創建的工廠方法)。
  private Streamable<Product> streamable;

  public MonetaryAmount getTotal() { 
    return streamable.stream() //包裝器類型公開了在Streamable<Product>上計算新值的附加API。
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }
}

interface ProductRepository implements Repository<Product, Long> {
    //包裝器類型可以直接用作查詢方法返回類型。不需要返回Stremable<Product>並手動將其封裝到存儲庫客戶機中。
  Products findAllByDescriptionContaining(String text); 
}

Vavr 集合的支持

Vavr是一個包含Java中函數式編程概念的庫。它附帶了一組可用作查詢方法返回類型的自定義集合類型。

Vavr 集合類型 Vavr 實現的類型 Valid 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類型(第三列)獲得作為實現類型的第二列中的類型。然後通過實現派生類的方法進行類型轉化。

7、空值方法處理庫

​ 在Spring Data 2.0中,返回單個聚合實例的存儲庫CRUD方法使用Java 8 s可選來指示可能缺少的值。除此之外,Spring Data還支持在查詢方法上返回以下包裝器類型:

  • com.google.common.base.Optional
  • scala.Option
  • io.vavr.control.Option

​ 或者,查詢方法可以選擇根本不使用包裝器類型。如果沒有查詢結果,則返回null。返回集合、集合替代、包裝器和流的存儲庫方法保證不會返回null,而是返回相應的空表示。有關詳細信息,請參見存儲庫查詢返回類型。

空值註解

您可以使用Spring Framework的可空性注釋來表示存儲庫方法的可空性約束。它們提供了一種工具友好的方法,並在運行時選擇空檢查,如下所示:

  • @NonNullApi:在包級別上使用,用於聲明參數和返回值的默認行為是不接受或生成空值。
  • @NonNull:用於不能為null的參數或返回值(在@NonNullApi應用的地方,參數和返回值不需要)。
  • @Nullable:用於可以為空的參數或返回值。

Spring注釋使用JSR 305注釋(一種停止維護但廣泛傳播的JSR)進行元注釋。JSR 305元注釋讓工具供應商(如IDEA、Eclipse和Kotlin)以通用的方式提供空安全支持,而不必對Spring注釋進行硬編碼支持。要啟用查詢方法的nullability約束的運行時檢查,您需要在package-info中使用Spring 的@NonNullApi來激活package-info.java上的非空配置,如下面的示例所示

例20:在包級別上聲明非空

@org.springframework.lang.NonNullApi
package com.acme;

​ 一旦設置了非空默認值,存儲庫查詢方法調用將在運行時驗證是否存在可空性約束。如果查詢執行結果違反定義的約束,則拋出異常。當方法將返回null,但聲明為不可空時(存儲庫所在的包上定義的注釋的默認值),就會發生這種情況。如果您希望再次選擇可為空的結果,可以在單個方法上有選擇地使用@Nullable。使用本節開始提到的結果包裝器類型繼續按預期工作:空結果被轉換為表示缺席的值。下面的示例顯示了剛才描述的許多技術:

例21:使用不同的空值配置

package com.acme;                                 // 存儲庫駐留在一個包(或子包)中,我們為其定義了非空行為。                     

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress);      //當執行的查詢不產生結果時,拋出EmptyResultDataAccessException。當傳遞給方法的電子郵件地址為空時,拋出IllegalArgumentException異常。              

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress);       //當執行的查詢沒有產生結果時,返回null。還接受null作為emailAddress的值。   

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); //當執行的查詢不產生結果時,返回Optional.empty()。當傳遞給方法的電子郵件地址為空時,拋出IllegalArgumentException異常。
}

8、Stream化查詢結果

​ 通過使用Java 8 Stream作為返回類型,可以漸進地處理查詢方法的結果。與將查詢結果包裝在流數據存儲中不同,使用特定的方法執行流,如下面的示例所示

例23:用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);

流可能包裝了底層數據存儲特定的資源,因此在使用後必須關閉。您可以使用close()方法手動關閉流,也可以使用Java 7 try-with-resources塊,如下面的示例所示

例24:try-catch形式使用Stream

try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}

9 、異步查詢結果

​ 通過使用Spring的異步方法執行能力,存儲庫查詢可以異步運行。這意味着,當實際的查詢執行發生在已提交給Spring TaskExecutor的任務中時,該方法在調用時立即返回。異步查詢執行與反應性查詢執行不同,不應該混合使用。有關響應性支持的更多細節,請參閱特定於存儲的文檔。下面的示例顯示了許多異步查詢

@Async
Future<User> findByFirstname(String firstname);               

@Async
CompletableFuture<User> findOneByFirstname(String firstname); 

@Async
ListenableFuture<User> findOneByLastname(String lastname);    
Tags: