Spring JPA 定義查詢方法

Kindear_chen發表於2020-09-05

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);    

相關文章