投影

簡介

Spring Data 查詢方法通常返回由倉庫管理的聚合根的一個或多個例項。然而,有時可能需要基於這些型別的某些屬性建立投影。Spring Data 允許建模專用的返回型別,以更選擇性地檢索受管聚合的部分檢視。

假設有一個倉庫和聚合根型別,例如以下示例:

示例聚合和倉庫
class Person {

  @Id UUID id;
  String firstname, lastname;
  Address address;

  static class Address {
    String zipCode, city, street;
  }
}

interface PersonRepository extends Repository<Person, UUID> {

  Collection<Person> findByLastname(String lastname);
}

現在假設我們只想檢索人員的名稱屬性。Spring Data 提供了哪些方法來實現這一點?本章的其餘部分將回答這個問題。

投影型別是位於實體型別層次結構之外的型別。實體實現的超類和介面位於型別層次結構之內,因此返回超型別(或實現的介面)將返回完全具體化實體的例項。

基於介面的投影

將查詢結果限制為僅名稱屬性的最簡單方法是宣告一個介面,該介面公開要讀取的屬性的訪問器方法,如以下示例所示:

用於檢索屬性子集的投影介面
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

這裡的關鍵在於,此處定義的屬性與聚合根中的屬性完全匹配。這樣做允許新增一個查詢方法,如下所示:

使用基於介面的投影和查詢方法的倉庫
interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

查詢執行引擎在執行時為返回的每個元素建立該介面的代理例項,並將對公開方法的呼叫轉發到目標物件。

在您的 Repository 中宣告一個覆蓋基本方法(例如在 CrudRepository、特定於儲存的倉庫介面或 Simple...Repository 中宣告)的方法,無論宣告的返回型別如何,都將導致對基本方法的呼叫。請確保使用相容的返回型別,因為基本方法不能用於投影。某些儲存模組支援 @Query 註解,將重寫的基本方法轉換為查詢方法,然後可用於返回投影。

投影可以遞迴使用。如果您還想包含一些 Address 資訊,請為其建立一個投影介面,並從 getAddress() 的宣告中返回該介面,如以下示例所示:

用於檢索屬性子集的投影介面
interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}

在方法呼叫時,將獲取目標例項的 address 屬性,並將其包裝成一個投影代理。

封閉式投影

一個投影介面,其所有訪問器方法都與目標聚合的屬性匹配,被認為是封閉式投影。以下示例(我們之前在本章中也使用過)是封閉式投影:

封閉式投影
interface NamesOnly {

  String getFirstname();
  String getLastname();
}

如果您使用封閉式投影,Spring Data 可以最佳化查詢執行,因為我們知道支援投影代理所需的所有屬性。有關更多詳細資訊,請參閱參考文件中特定於模組的部分。

開放式投影

投影介面中的訪問器方法也可以透過使用 @Value 註解來計算新值,如以下示例所示:

開放式投影
interface NamesOnly {

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
  …
}

支援投影的聚合根在 target 變數中可用。使用 @Value 的投影介面是一個開放式投影。在這種情況下,Spring Data 無法應用查詢執行最佳化,因為 SpEL 表示式可以使用聚合根的任何屬性。

@Value 中使用的表示式不應過於複雜——您應該避免在 String 變數中進行程式設計。對於非常簡單的表示式,一種選擇是使用預設方法(在 Java 8 中引入),如以下示例所示:

使用預設方法實現自定義邏輯的投影介面
interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname().concat(" ").concat(getLastname());
  }
}

這種方法要求您能夠純粹基於投影介面上公開的其他訪問器方法來實現邏輯。第二種更靈活的選擇是在 Spring bean 中實現自定義邏輯,然後從 SpEL 表示式中呼叫它,如以下示例所示:

示例 Person 物件
@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

請注意,SpEL 表示式如何引用 myBean 並呼叫 getFullName(…) 方法,並將投影目標作為方法引數轉發。由 SpEL 表示式評估支援的方法也可以使用方法引數,然後可以在表示式中引用這些引數。方法引數透過名為 argsObject 陣列可用。以下示例顯示瞭如何從 args 陣列中獲取方法引數:

示例 Person 物件
interface NamesOnly {

  @Value("#{args[0] + ' ' + target.firstname + '!'}")
  String getSalutation(String prefix);
}

同樣,對於更復雜的表示式,您應該使用 Spring bean 並讓表示式呼叫方法,如前面所述。

可空包裝器

投影介面中的 getter 可以利用可空包裝器來提高空安全性。目前支援的包裝器型別有:

  • java.util.Optional

  • com.google.common.base.Optional

  • scala.Option

  • io.vavr.control.Option

使用可空包裝器的投影介面
interface NamesOnly {

  Optional<String> getFirstname();
}

如果底層投影值不為 null,則使用包裝器型別的存在表示返回這些值。如果支援值為 null,則 getter 方法返回所用包裝器型別的空表示。

基於類的投影(DTO)

定義投影的另一種方法是使用值型別 DTO(資料傳輸物件),它們包含要檢索的欄位的屬性。這些 DTO 型別可以像投影介面一樣使用,只是不發生代理,也無法應用巢狀投影。

如果儲存透過限制要載入的欄位來最佳化查詢執行,則要載入的欄位將從公開的建構函式的引數名稱中確定。

以下示例顯示了一個投影 DTO:

投影 DTO
record NamesOnly(String firstname, String lastname) {
}

Java 記錄非常適合定義 DTO 型別,因為它們遵循值語義:所有欄位都是 private final,並且自動建立 equals(…)/hashCode()/toString() 方法。或者,您可以使用任何定義要投影的屬性的類。

動態投影

到目前為止,我們已經將投影型別用作返回型別或集合的元素型別。但是,您可能希望在呼叫時選擇要使用的型別(這使其具有動態性)。要應用動態投影,請使用如下所示的查詢方法:

使用動態投影引數的倉庫
interface PersonRepository extends Repository<Person, UUID> {

  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

這樣,該方法可以用於按原樣獲取聚合或應用投影后獲取聚合,如以下示例所示:

使用帶有動態投影的倉庫
void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}
型別為 Class 的查詢引數將檢查它們是否符合動態投影引數的條件。如果查詢的實際返回型別等於 Class 引數的泛型引數型別,則匹配的 Class 引數不可用於查詢或 SpEL 表示式中使用。如果您希望將 Class 引數用作查詢引數,請確保使用不同的泛型引數,例如 Class<?>

當使用基於類的投影時,型別必須宣告單個建構函式,以便 Spring Data 可以確定它們的輸入屬性。如果您的類定義了多個建構函式,則不能在沒有進一步提示的情況下將該型別用於 DTO 投影。在這種情況下,請用 @PersistenceCreator 註解所需的建構函式,如下所示,以便 Spring Data 可以確定要選擇哪些屬性:

public class NamesOnly {

  private final String firstname;
  private final String lastname;

  protected NamesOnly() { }

  @PersistenceCreator
  public NamesOnly(String firstname, String lastname) {
      this.firstname = firstname;
      this.lastname = lastname;
  }

  // ...
}

使用 JPA 投影

您可以透過多種方式使用 JPA 投影。根據技術和查詢型別,您需要應用特定的注意事項。

Spring Data JPA 通常使用 Tuple 查詢來為基於介面的投影構建介面代理。

派生查詢

查詢派生透過內省返回型別來支援基於類和基於介面的投影。基於類的投影使用 JPA 的例項化機制(建構函式表示式)來建立投影例項。

投影將選擇限制在目標實體的頂級屬性。任何解析為連線的巢狀屬性都會選擇整個巢狀屬性,導致完全連線具象化。

基於字串的查詢

對基於字串的查詢的支援涵蓋了 JPQL 查詢 (@Query) 和原生查詢 (@NativeQuery)。

JPQL 查詢

JPA 使用 JPQL 返回基於類的投影的機制是建構函式表示式。因此,您的查詢必須定義一個建構函式表示式,例如 SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u。(請注意 DTO 型別使用了 FQDN!)此 JPQL 表示式也可以在 @Query 註解中使用,您可以在其中定義任何命名查詢。作為一種變通方法,您可以使用帶有 ResultSetMapping 的命名查詢或 Hibernate 特定的 ResultListTransformer

如果您的查詢選擇主實體或選擇項列表,Spring Data JPA 可以幫助您將查詢重寫為建構函式表示式。

DTO 投影 JPQL 查詢重寫

JPQL 查詢允許透過建構函式表示式選擇根物件、單個屬性和 DTO 物件。使用建構函式表示式可以快速為查詢新增大量文字,並使其難以閱讀實際查詢。Spring Data JPA 可以透過引入建構函式表示式來方便您處理 JPQL 查詢。

考慮以下查詢

示例 1. 投影查詢
interface UserRepository extends Repository<User, Long> {

  @Query("SELECT u FROM USER u WHERE u.lastname = :lastname")                       (1)
  List<UserDto> findByLastname(String lastname);

  @Query("SELECT u.firstname, u.lastname FROM USER u WHERE u.lastname = :lastname") (2)
  List<UserDto> findMultipleColumnsByLastname(String lastname);
}

record UserDto(String firstname, String lastname){}
1 選擇頂級實體。此查詢將被重寫為 SELECT new UserDto(u.firstname, u.lastname) FROM USER u WHERE u.lastname = :lastname
2 多選 firstnamelastname 屬性。此查詢將被重寫為 SELECT new UserDto(u.firstname, u.lastname) FROM USER u WHERE u.lastname = :lastname

JPQL 建構函式表示式不能包含所選列的別名,查詢重寫不會為您刪除它們。雖然 SELECT u as user, count(u.roles) as roleCount FROM USER u … 是基於介面的投影(依賴於返回的 Tuple 中的列名)的有效查詢,但當請求 DTO 時,相同的構造是無效的,此時它需要是 SELECT u, count(u.roles) FROM USER u …
一些持久化提供程式可能對此寬容,而另一些則不然。

返回 DTO 投影型別(域型別層次結構之外的 Java 型別)的倉庫查詢方法需要進行查詢重寫。如果 @Query 註解的查詢已使用建構函式表示式,則 Spring Data 會回退並且不應用 DTO 建構函式表示式重寫。

確保您的 DTO 型別為投影提供一個全參建構函式,否則查詢將失敗。

原生查詢

使用基於類的投影時,根據您的具體情況,其使用需要更多考慮。

  • 如果結果型別的屬性直接對映到結果(列的順序及其型別與建構函式引數匹配),那麼您可以將查詢結果型別宣告為 DTO 型別,而無需進一步提示(或透過動態投影使用 DTO 類)。

  • 如果屬性不匹配或需要轉換,請透過 JPA 的註解使用 @SqlResultSetMapping 將結果集對映到 DTO,並透過 @NativeQuery(resultSetMapping = "…") 提供結果對映名稱。

© . This site is unofficial and not affiliated with VMware.