投影和摘录
Spring Data REST 提供了您导出域模型的默认视图。但是,有时您可能需要出于各种原因更改该模型的视图。本节介绍如何定义投影和摘录以提供资源的简化和缩减视图。
投影
考虑以下域模型
@Entity
public class Person {
@Id @GeneratedValue
private Long id;
private String firstName, lastName;
@OneToOne
private Address address;
…
}
上述示例中的 Person
对象具有多个属性
-
id
是主键。 -
firstName
和lastName
是数据属性。 -
address
是指向另一个域对象的链接。
现在假设我们创建了一个相应的资源库,如下所示
interface PersonRepository extends CrudRepository<Person, Long> {}
默认情况下,Spring Data REST 导出此域对象,包括其所有属性。firstName
和 lastName
导出为它们本身的简单数据对象。关于 address
属性有两个选项。一个选项是也为 Address
对象定义一个资源库,如下所示
interface AddressRepository extends CrudRepository<Address, Long> {}
在这种情况下,Person
资源将 address
属性呈现为其相应 Address
资源的 URI。如果我们要在系统中查找“Frodo”,我们可以预期看到如下所示的 HAL 文档
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"_links" : {
"self" : {
"href" : "https://127.0.0.1:8080/persons/1"
},
"address" : {
"href" : "https://127.0.0.1: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://127.0.0.1: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
投影仅对 firstName
和 lastName
具有 getter,这意味着它不提供任何地址信息。假设您有 Address
资源的单独资源库,则来自 Spring Data REST 的默认视图与之前的表示略有不同,如下例所示
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"_links" : {
"self" : {
"href" : "https://127.0.0.1:8080/persons/1{?projection}", (1)
"templated" : true (2)
},
"address" : {
"href" : "https://127.0.0.1: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 文档的这一部分显示了有关 GET 和 Person 资源的详细信息。 |
2 | 这部分包含 projection 选项。 |
3 | 这部分包含 noAddresses 投影。 |
4 | 此投影实际提供的属性包括 firstName 和 lastName 。 |
如果投影定义
|
引入隐藏数据
到目前为止,在本节中,我们介绍了如何使用投影来减少呈现给用户的信息。投影还可以引入通常不可见的数据。例如,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 表达式,该表达式获取目标对象并将它的 firstName 和 lastName 属性拼接在一起,以呈现一个只读的 fullName 。 |
摘录
摘录是一种自动应用于资源集合的投影。例如,您可以更改 PersonRepository
如下
@RepositoryRestResource(excerptProjection = NoAddresses.class)
interface PersonRepository extends CrudRepository<Person, Long> {}
前面的示例指示 Spring Data REST 在将 Person
资源嵌入到集合或相关资源中时使用 NoAddresses
投影。
摘录投影不会自动应用于单个资源。它们必须被刻意应用。摘录投影旨在提供集合数据的默认预览,而不是在获取单个资源时使用。有关此主题的讨论,请参阅 为什么摘录投影不会自动应用于 Spring Data REST 项目资源?。 |
除了更改默认渲染之外,摘录还有其他渲染选项,如下一节所示。
摘录常用数据
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://127.0.0.1:8080/persons/1"
},
"address" : { (2)
"href" : "https://127.0.0.1:8080/persons/1/address"
}
}
}
1 | address 数据直接内联包含,因此您不必导航即可获取它。 |
2 | 到 Address 资源的链接仍然提供,因此仍然可以导航到其自己的资源。 |
请注意,前面的示例是本章前面显示的示例的混合。您可能需要重新阅读它们以了解最终示例的演变过程。
为存储库配置 @RepositoryRestResource(excerptProjection=…) 会更改默认行为。如果您已经发布了版本,这可能会对服务消费者造成重大更改。 |