Projections and Excerpts

Spring Data REST 提供了你导出的领域模型的默认视图。不过,有时,你可能需要出于各种原因更改该模型的视图。本部分介绍如何定义投影和摘要,以提供资源的简化和精简视图。

Projections

考虑以下领域模型:

@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" : "http://localhost:8080/persons/1"
    },
    "address" : {
      "href" : "http://localhost: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" : "http://localhost: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 It exports the firstName.
4 It exports the lastName.

NoAddresses 投影仅有 firstNamelastName 的 getter,这意味着它不会提供任何地址信息。假设你为 Address 资源有一个单独的存储库,则 Spring Data REST 的默认视图与以前的表示略有不同,如下例所示:

{
  "firstName" : "Frodo",
  "lastName" : "Baggins",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/persons/1{?projection}", 1
      "templated" : true 2
    },
    "address" : {
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}
1 此资源有一个新选项:{?projection}
2 self URI 是 URI 模板。

若要查看对资源的投影,请查找 http://localhost:8080/persons/1?projection=noAddresses

提供给 projection 查询参数的值与 @Projection(name = "noAddress") 中指定的值相同。它与投影接口的名称无关。

您可以有多个投影。

请参阅“ Projections”以查看一个示例项目。我们鼓励您尝试一下。

Spring Data REST 以如下方式查找投影定义:

  • 在与你的实体定义(或其子包之一)位于同一包中的任何 @Projection 接口中找到的接口都已注册。

  • 你可以手动使用 RepositoryRestConfiguration.getProjectionConfiguration().addProjection(…) 注册投影。

无论哪种情况,投影接口都必须具有 @Projection 注解。

Finding Existing Projections

Spring Data REST 公开了 Application-Level Profile Semantics (ALPS) 文档,即微型元数据格式。要查看 ALPS 元数据,请访问根资源公开的 profile 链接。如果向下导航到 Person 资源(为 /alps/persons)的 ALPS 文档,您将找到关于 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(…) 手动注册。

Bringing in Hidden Data

本部分到目前为止,我们已经介绍了如何使用投影来减少呈现给用户的信息。投影也可以引入通常不可见的数据。例如,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 集成。如果您创建一个 UserRepository,通常会导出 password 字段,这并不好。在前一个示例中,通过在 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

Excerpts

摘录是一种自动应用于资源集合的投影。例如,您可以如下更改 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?”。

除了更改默认呈现之外,摘要还有其他呈现选项,如下一部分所示。

Excerpting Commonly Accessed Data

当您组合域对象时,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" : "http://localhost:8080/persons/1"
    },
    "address" : { 2
      "href" : "http://localhost:8080/persons/1/address"
    }
  }
}
1 address 数据直接包含在内联中,因此你无需导航即可获取它。
2 仍然提供到 Address 资源的链接,这使得仍然可以导航到其自身的资源。

注意,前一个示例混合了本章前面章节中所示的示例。您可能需要回头阅读它们,了解最终示例的进展。

为存储库配置`@RepositoryRestResource(excerptProjection=…​)`会更改默认行为。如果你已经发布了服务,这有可能会对你的服务使用者导致重大更改。