投影
投影
Spring Data 查询方法通常返回由 repository 管理的 aggregate root 的一个或多个实例。然而,有时基于这些类型的某些属性创建 projections 可能更为理想。Spring Data 允许建模专门的返回类型,从而更有选择性地检索 managed aggregates 的部分视图。
想象一个仓储和聚合根类型,如下面的示例所示:
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);
}
现在想象一下,我们只想获取 person 的 name 属性。Spring Data 提供了哪些手段来实现这一点?本章的其余部分将回答这个问题。
投影类型是位于实体类型层次结构之外的类型。实体所继承的父类和所实现的接口位于类型层次结构内部,因此返回超类型(或已实现接口)会返回完全物化实体的实例。
基于接口的投影
限制查询结果只包含 name 属性的最简单方式是声明一个接口,该接口为需要读取的属性暴露访问器方法,如下例所示:
interface NamesOnly {
String getFirstname();
String getLastname();
}
这里重要的是,此处定义的属性与 aggregate root 中的属性完全匹配。这样做就可以按如下方式添加一个查询方法:
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
查询执行引擎会在运行时为每个返回的元素创建该接口的代理实例,并将对公开方法的调用转发给目标对象。
在 Repository 中声明一个覆盖基础方法(例如在 CrudRepository、store 特定的 repository 接口或 Simple…Repository 中声明的方法)会导致调用基础方法,而不管声明的返回类型是什么。请确保使用兼容的返回类型,因为基础方法不能用于 projections。某些 store 模块支持 @Query 注解,可将覆盖的基础方法转换为 query method,从而可用于返回 projections。
投影可以递归使用。如果你还想包含部分 Address 信息,请为其创建一个投影接口,并在 getAddress() 的声明中返回该接口,如下例所示:
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
在方法调用时,会获取目标实例的 address 属性,并依次将其包装为 projecting proxy。
Closed Projections
一个所有 accessor methods 都匹配 target aggregate 的 properties 的 projection interface 被认为是 closed projection。以下示例(我们在本章前面也用过)是一个 closed projection:
interface NamesOnly {
String getFirstname();
String getLastname();
}
如果您使用 closed projection,Spring Data 可以优化 query execution,因为我们知道支撑 projection proxy 所需的所有 attributes。有关更多详细信息,请参阅参考文档的 module-specific 部分。
Open Projections
投影接口中的访问器方法也可以通过使用 @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 表达式中调用它,如下例所示:
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
注意 SpEL 表达式如何引用 myBean 并调用 getFullName(…) 方法,并将投影目标作为方法参数传递。由 SpEL 表达式求值支持的方法也可以使用方法参数,然后可以在表达式中引用这些参数。方法参数通过名为 args 的 Object 数组提供。以下示例展示如何从 args 数组获取方法参数:
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
再次说明,对于更复杂的表达式,你应该使用 Spring bean,并让表达式调用方法,如前文所述。
Nullable Wrappers
projection interfaces 中的 Getters 可以使用 nullable wrappers 来提升 null-safety。当前支持的 wrapper types 有:
-
java.util.Optional -
com.google.common.base.Optional -
scala.Option -
io.vavr.control.Option
interface NamesOnly {
Optional<String> getFirstname();
}
如果底层投影值不为 null,则返回值采用 wrapper type 的 present-representation。如果 backing value 为 null,则 getter method 返回所用 wrapper type 的 empty representation。
基于类的投影(DTOs)
另一种定义 projections 的方式是使用 value type DTOs (Data Transfer Objects),它们为需要检索的字段保存属性。这些 DTO 类型的使用方式与 projection interfaces 完全相同,只是不会发生 proxying,也不能应用 nested projections。
如果 store 通过限制要加载的字段来优化查询执行,则要加载的字段由所暴露的 constructor 的参数名称确定。
下面的示例展示了一个 projecting DTO:
record NamesOnly(String firstname, String lastname) {
}
Java Records 非常适合定义 DTO 类型,因为它们遵循值语义:所有字段均为 private final,并且会自动创建 equals(…)/hashCode()/toString() 方法。或者,你可以使用任何定义了所需投影属性的类。
动态 Projections
目前,我们已将 projection type 用作集合的 return type 或 element type。不过,你可能希望在 invocation time 选择要使用的类型(这使其具有动态性)。要应用 dynamic projections,请使用查询方法,如下例所示:
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
这样,该方法可用于按原样获取 aggregates,或应用 projection 后获取,如下例所示:
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;
}
// ...
}