投影和摘錄

Spring Data REST 提供您匯出的領域模型的預設檢視。然而,有時您可能需要出於各種原因更改該模型的檢視。本節介紹如何定義投影(projections)和摘錄(excerpts)以提供資源的簡化和縮減檢視。

投影

考慮以下領域模型

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
  …
}

前例中的 Person 物件有幾個屬性

  • id 是主鍵。

  • firstNamelastName 是資料屬性。

  • address 是指向另一個領域物件的連結。

現在假設我們建立了一個相應的倉庫,如下所示

interface PersonRepository extends CrudRepository<Person, Long> {}

預設情況下,Spring Data REST 會匯出這個領域物件,包括其所有屬性。firstNamelastName 作為普通資料物件匯出。關於 address 屬性有兩種選擇。一種選擇是也為 Address 物件定義一個倉庫,如下所示

interface AddressRepository extends CrudRepository<Address, Long> {}

在這種情況下,一個 Person 資源將其 address 屬性渲染為其相應 Address 資源的 URI。如果我們在系統中查詢“Frodo”,我們可以預期看到一個類似於以下的 HAL 文件

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "https://:8080/persons/1"
    },
    "address" : {
      "href" : "https://:8080/persons/1/address"
    }
  }
}

還有另一種方式。如果 Address 領域物件沒有自己的倉庫定義,Spring Data REST 會將資料欄位包含在 Person 資源中,如下例所示

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : {
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "https://:8080/persons/1"
    }
  }
}

但是,如果您根本不需要 address 詳情怎麼辦?同樣,預設情況下,Spring Data REST 會匯出其所有屬性(除了 id)。您可以透過定義一個或多個投影來為您的 REST 服務消費者提供替代方案。以下示例顯示了一個不包含地址的投影

@Projection(name = "noAddresses", types = { Person.class }) (1)
interface NoAddresses { (2)

  String getFirstName(); (3)

  String getLastName(); (4)
}
1 @Projection 註解將其標記為投影。name 屬性提供投影的名稱,我們稍後會更詳細地介紹。types 屬性指定此投影僅應用於 Person 物件。
2 它是一個 Java 介面,使其具有宣告性。
3 它匯出 firstName
4 它匯出 lastName

NoAddresses 投影僅包含 firstNamelastName 的 getter,這意味著它不會提供任何地址資訊。假設您有一個單獨的 Address 資源倉庫,Spring Data REST 的預設檢視與之前的表示略有不同,如下例所示

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "https://:8080/persons/1{?projection}", (1)
      "templated" : true (2)
    },
    "address" : {
      "href" : "https://:8080/persons/1/address"
    }
  }
}
1 此資源有一個新選項:{?projection}
2 self URI 是一個 URI 模板。

要檢視資源的投影,查詢 localhost:8080/persons/1?projection=noAddresses

提供給 projection 查詢引數的值與 @Projection(name = "noAddress") 中指定的值相同。它與投影介面的名稱無關。

您可以有多個投影。

請參閱 投影 檢視示例專案。我們鼓勵您進行實驗。

Spring Data REST 按如下方式查詢投影定義

  • 在您的實體定義所在包(或其子包之一)中找到的任何 @Projection 介面都會被註冊。

  • 您可以使用 RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…) 手動註冊投影。

在任何一種情況下,投影介面都必須具有 @Projection 註解。

查詢現有投影

Spring Data REST 暴露 應用級配置檔案語義 (ALPS) 文件,這是一種微元資料格式。要檢視 ALPS 元資料,請遵循根資源暴露的 profile 連結。如果您導航到 Person 資源的 ALPS 文件(即 /alps/persons),您可以找到有關 Person 資源的許多詳細資訊。投影會列出,並與 GET REST 過渡的詳細資訊一起,出現在類似於以下示例的塊中

{ …
  "id" : "get-person", (1)
  "name" : "person",
  "type" : "SAFE",
  "rt" : "#person-representation",
  "descriptors" : [ {
    "name" : "projection", (2)
    "doc" : {
      "value" : "The projection that shall be applied when rendering the response. Acceptable values available in nested descriptors.",
      "format" : "TEXT"
    },
    "type" : "SEMANTIC",
    "descriptors" : [ {
      "name" : "noAddresses", (3)
      "type" : "SEMANTIC",
      "descriptors" : [ {
        "name" : "firstName", (4)
        "type" : "SEMANTIC"
      }, {
        "name" : "lastName", (4)
        "type" : "SEMANTIC"
      } ]
    } ]
  } ]
},
…
1 ALPS 文件的這一部分顯示了關於 GETPerson 資源的詳細資訊。
2 這一部分包含 projection 選項。
3 這一部分包含 noAddresses 投影。
4 此投影實際提供的屬性包括 firstNamelastName

投影定義會被拾取並提供給客戶端,如果它們

  • 使用 @Projection 註解標記,並且位於域型別所在的同一包(或子包)中,或者

  • 使用 RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…) 手動註冊。

引入隱藏資料

到目前為止,本節介紹瞭如何使用投影來減少呈現給使用者的資訊。投影還可以引入通常不可見的資料。例如,Spring Data REST 會忽略用 @JsonIgnore 註解標記的欄位或 getter。考慮以下領域物件

@Entity
public class User {

	@Id @GeneratedValue
	private Long id;
	private String name;

	@JsonIgnore private String password; (1)

	private String[] roles;
  …
1 Jackson 的 @JsonIgnore 用於阻止將 password 欄位序列化為 JSON。

前例中的 User 類可用於儲存使用者資訊以及與 Spring Security 的整合。如果您建立一個 UserRepositorypassword 欄位通常會被匯出,這是不好的。在前例中,我們透過在 password 欄位上應用 Jackson 的 @JsonIgnore 來防止這種情況發生。

如果 @JsonIgnore 位於欄位相應的 getter 函式上,Jackson 也不會將該欄位序列化為 JSON。

然而,投影引入了仍然提供此欄位的能力。可以建立以下投影

@Projection(name = "passwords", types = { User.class })
interface PasswordProjection {

  String getPassword();
}

如果建立並使用此類投影,它將繞過放置在 User.password 上的 @JsonIgnore 指令。

這個例子可能看起來有點牽強,但對於更豐富的領域模型和許多投影來說,可能會意外洩露此類細節。由於 Spring Data REST 無法辨別此類資料的敏感性,因此避免這種情況取決於您自己。

投影還可以生成虛擬資料。想象一下您有以下實體定義

@Entity
public class Person {

  ...
  private String firstName;
  private String lastName;

  ...
}

您可以建立一個投影,將前例中的兩個資料欄位組合在一起,如下所示

@Projection(name = "virtual", types = { Person.class })
public interface VirtualProjection {

  @Value("#{target.firstName} #{target.lastName}") (1)
  String getFullName();

}
1 Spring 的 @Value 註解允許您插入一個 SpEL 表示式,該表示式獲取目標物件並將其 firstNamelastName 屬性拼接在一起以渲染一個只讀的 fullName

摘錄

摘錄是一種自動應用於資源集合的投影。例如,您可以如下更改 PersonRepository

@RepositoryRestResource(excerptProjection = NoAddresses.class)
interface PersonRepository extends CrudRepository<Person, Long> {}

前例指示 Spring Data REST 在將 Person 資源嵌入集合或相關資源時使用 NoAddresses 投影。

摘錄投影不會自動應用於單個資源。它們必須被刻意應用。摘錄投影旨在提供集合資料的預設預覽,但在獲取單個資源時不適用。請參閱 Why is an excerpt projection not applied automatically for a Spring Data REST item resource? 獲取關於此主題的討論。

除了更改預設渲染外,摘錄還具有下一節所示的額外渲染選項。

摘錄常用訪問資料

REST 服務中的一種常見情況發生在您組合領域物件時。例如,一個 Person 儲存在一個表中,而其相關的 Address 儲存在另一個表中。預設情況下,Spring Data REST 將人員的 address 作為客戶端必須導航的 URI 提供。但是,如果消費者經常需要獲取這額外的部分資料,摘錄投影可以將這額外的資料內聯顯示,為您節省一次額外的 GET 請求。要做到這一點,您可以定義另一個摘錄投影,如下所示

@Projection(name = "inlineAddress", types = { Person.class }) (1)
interface InlineAddress {

  String getFirstName();

  String getLastName();

  Address getAddress(); (2)
}
1 此投影被命名為 inlineAddress
2 此投影添加了 getAddress,它返回 Address 欄位。在投影內部使用時,它會導致資訊內聯包含。

您可以將其插入到 PersonRepository 定義中,如下所示

@RepositoryRestResource(excerptProjection = InlineAddress.class)
interface PersonRepository extends CrudRepository<Person, Long> {}

這樣做會導致 HAL 文件如下所示

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "address" : { (1)
    "street": "Bag End",
    "state": "The Shire",
    "country": "Middle Earth"
  },
  "_links" : {
    "self" : {
      "href" : "https://:8080/persons/1"
    },
    "address" : { (2)
      "href" : "https://:8080/persons/1/address"
    }
  }
}
1 address 資料直接內聯包含,因此您無需導航即可獲取。
2 指向 Address 資源的連結仍然提供,因此仍然可以導航到其自己的資源。

請注意,前面的示例是本章前面所示示例的混合。您可能需要回顧這些示例以瞭解演變到最終示例的過程。

為倉庫配置 @RepositoryRestResource(excerptProjection=…​) 會改變預設行為。如果您已經發布了一個版本,這可能會導致服務消費者出現破壞性變更。