Projections
Spring 数据查询方法通常返回存储库管理的聚合根的一个或多个实例。然而,有时可能需要基于这些类型的特定属性创建投影。Spring 数据允许建模专用返回类型,以更选择性地检索管理聚合的局部视图。
Spring Data query methods usually return one or multiple instances of the aggregate root managed by the repository. However, it might sometimes be desirable to create projections based on certain attributes of those types. Spring Data allows modeling dedicated return types, to more selectively retrieve partial views of the managed aggregates.
设想一个存储库和一个聚合根类型,如下例所示:
Imagine a repository and aggregate root type such as the following example:
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 数据提供哪些方法来实现此目的?本章的其余部分将回答这个问题。
Now imagine that we want to retrieve the person’s name attributes only. What means does Spring Data offer to achieve this? The rest of this chapter answers that question.
Interface-based Projections
将查询结果仅限制为姓名属性的最简单方法是声明一个公开要读取的属性的访问器方法的接口,如下例所示:
The easiest way to limit the result of the queries to only the name attributes is by declaring an interface that exposes accessor methods for the properties to be read, as shown in the following example:
interface NamesOnly {
String getFirstname();
String getLastname();
}
这里的重要部分是此处定义的属性与聚合根中的属性完全匹配。这样做可以添加一个查询方法,如下所示:
The important bit here is that the properties defined here exactly match properties in the aggregate root. Doing so lets a query method be added as follows:
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
查询执行引擎在运行时为每个返回的元素创建该接口的代理实例,并将对公开方法的调用转发到目标对象。
The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object.
在 |
Declaring a method in your |
投影可以递归使用。如果你还想包含一些 Address
信息,请为此创建一个投影接口,并从 getAddress()
的声明中返回该接口,如下例所示:
Projections can be used recursively. If you want to include some of the Address
information as well, create a projection interface for that and return that interface from the declaration of getAddress()
, as shown in the following example:
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
在调用方法时,将获取目标实例的 address
属性,并将其依次封装到投影代理中。
On method invocation, the address
property of the target instance is obtained and wrapped into a projecting proxy in turn.
Closed Projections
如果一个投影接口的访问器方法都匹配目标聚合的属性,那么该接口被认为是闭合投影。以下示例(我们也在本章中使用过)是一个闭合投影:
A projection interface whose accessor methods all match properties of the target aggregate is considered to be a closed projection. The following example (which we used earlier in this chapter, too) is a closed projection:
interface NamesOnly {
String getFirstname();
String getLastname();
}
如果你使用一个闭合投影,Spring Data 可以优化查询执行,因为我们知道所有用于支持投影代理的属性。有关更多详细信息,请参阅参考文档的模块特定部分。
If you use a closed projection, Spring Data can optimize the query execution, because we know about all the attributes that are needed to back the projection proxy. For more details on that, see the module-specific part of the reference documentation.
Open Projections
投影接口中的访问器方法还可以使用 @Value
注解来计算新值,如下例所示:
Accessor methods in projection interfaces can also be used to compute new values by using the @Value
annotation, as shown in the following example:
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
支持投影的聚合根可以在 target
变量中获得。使用 @Value
的投影接口是一个开放投影。Spring Data 不能在这种情况中应用查询执行优化,因为 SpEL 表达式可以对聚合根的任何属性进行操作。
The aggregate root backing the projection is available in the target
variable.
A projection interface using @Value
is an open projection.
Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.
在 @Value
中使用的表达式并不应该过于复杂,你应该避免在 String
变量中进行编程。对于非常简单的表达式,可以使用默认方法(在 Java 8 中引入)来解决,如下例所示:
The expressions used in @Value
should not be too complex — you want to avoid programming in String
variables.
For very simple expressions, one option might be to resort to default methods (introduced in Java 8), as shown in the following example:
interface NamesOnly {
String getFirstname();
String getLastname();
default String getFullName() {
return getFirstname().concat(" ").concat(getLastname());
}
}
该方法需要你可以基于投影接口上公开的其他访问器方法来实现逻辑。第二种更灵活的方法是在 Spring bean 中实现自定义逻辑,然后从 SpEL 表达式中对其进行调用,如下例所示:
This approach requires you to be able to implement logic purely based on the other accessor methods exposed on the projection interface. A second, more flexible, option is to implement the custom logic in a Spring bean and then invoke that from the SpEL expression, as shown in the following example:
@Component
class MyBean {
String getFullName(Person person) {
…
}
}
interface NamesOnly {
@Value("#{@myBean.getFullName(target)}")
String getFullName();
…
}
请注意 SpEL 表达式如何引用 myBean
并调用 getFullName(…)
方法,并将投影目标作为方法参数转发。由 SpEL 表达式求值支持的方法还可以使用可以从表达式引用的方法参数。方法参数可以通过名为 args
的 Object
数组获得。以下示例展示了如何从 args
数组中获取方法参数:
Notice how the SpEL expression refers to myBean
and invokes the getFullName(…)
method and forwards the projection target as a method parameter.
Methods backed by SpEL expression evaluation can also use method parameters, which can then be referred to from the expression.
The method parameters are available through an Object
array named args
. The following example shows how to get a method parameter from the args
array:
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
对于更复杂的表达式,你应该使用 Spring bean,并让表达式调用一个方法,如 projections.interfaces.open.bean-reference 所述。
Again, for more complex expressions, you should use a Spring bean and let the expression invoke a method, as described projections.interfaces.open.bean-reference.
Nullable Wrappers
投影接口中的 getter 可以使用可为空的包装器来提高 null 安全性。当前支持的包装器类型有:
Getters in projection interfaces can make use of nullable wrappers for improved null-safety. Currently supported wrapper types are:
-
java.util.Optional
-
com.google.common.base.Optional
-
scala.Option
-
io.vavr.control.Option
interface NamesOnly {
Optional<String> getFirstname();
}
如果底层投影值不是 null
,则使用包装器类型的当前表示形式返回值。如果支持值是 null
,则 getter 方法将返回所用包装器的空表示形式。
If the underlying projection value is not null
, then values are returned using the present-representation of the wrapper type.
In case the backing value is null
, then the getter method returns the empty representation of the used wrapper type.
Class-based Projections (DTOs)
定义投影的另一种方法是使用包含要检索的字段的属性的值类型 DTO(数据传输对象)。这些 DTO 类型可以与投影接口使用的方式完全相同,但不会进行代理并且不能应用嵌套投影。
Another way of defining projections is by using value type DTOs (Data Transfer Objects) that hold properties for the fields that are supposed to be retrieved. These DTO types can be used in exactly the same way projection interfaces are used, except that no proxying happens and no nested projections can be applied.
如果存储通过限制要加载的字段来优化查询执行,则要加载的字段由公开的构造函数的参数名确定。
If the store optimizes the query execution by limiting the fields to be loaded, the fields to be loaded are determined from the parameter names of the constructor that is exposed.
以下示例展示了一个投影 DTO:
The following example shows a projecting DTO:
record NamesOnly(String firstname, String lastname) {
}
Java 记录非常适合定义 DTO 类型,因为它们遵循值语义:所有字段都是 private final
,并且自动创建 equals(…)
/hashCode()
/toString()
方法。或者,可以使用定义要投影的属性的任何类。
Java Records are ideal to define DTO types since they adhere to value semantics:
All fields are private final
and equals(…)
/hashCode()
/toString()
methods are created automatically.
Alternatively, you can use any class that defines the properties you want to project.
Dynamic Projections
到目前为止,我们已经使用投影类型作为集合的返回类型或元素类型。然而,你可能需要在调用时选择要使用的类型(这会使其变成动态类型)。要应用动态投影,请使用查询方法,如以下示例所示:
So far, we have used the projection type as the return type or element type of a collection. However, you might want to select the type to be used at invocation time (which makes it dynamic). To apply dynamic projections, use a query method such as the one shown in the following example:
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
通过这种方式,该方法可用于按照原样获取聚合或应用投影,如下例所示:
This way, the method can be used to obtain the aggregates as is or with a projection applied, as shown in the following example:
void someMethod(PersonRepository people) {
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}
会检查类型为 |
Query parameters of type |