Spring Data Commons 允许通过投影定义自定义的查询结果,从而能够只请求必需的数据,提高应用程序的性能和可扩展性。投影可以是接口或类,用于指定要包含在查询结果中的字段。通过使用投影,开发人员可以优化数据传输,减少网络开销,并提高整体应用程序效率。此外,投影可以简化领域模型,避免不必要的属性加载,从而进一步提高性能。

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:

A sample aggregate and repository
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:

A projection interface to retrieve a subset of attributes
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:

A repository using an interface based projection with a query method
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.

Repository 中声明一个覆盖基本方法(例如,在 CrudRepository 中声明,存储特定存储库接口或 Simple…Repository 中声明)的方法,将导致调用基本方法,而不管声明的返回类型如何。请务必使用兼容的返回类型,因为基本方法不能用于投影。一些存储模块支持 @Query 注释,可将覆盖的基本方法转换成查询方法,然后可用于返回投影。

Declaring a method in your Repository that overrides a base method (e.g. declared in CrudRepository, a store-specific repository interface, or the Simple…Repository) results in a call to the base method regardless of the declared return type. Make sure to use a compatible return type as base methods cannot be used for projections. Some store modules support @Query annotations to turn an overridden base method into a query method that then can be used to return projections.

投影可以递归使用。如果你还想包含一些 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:

A projection interface to retrieve a subset of attributes
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:

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:

An Open Projection
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:

A projection interface using a default method for custom logic
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:

Sample Person object
@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

请注意 SpEL 表达式如何引用 myBean 并调用 getFullName(…​) 方法,并将投影目标作为方法参数转发。由 SpEL 表达式求值支持的方法还可以使用可以从表达式引用的方法参数。方法参数可以通过名为 argsObject 数组获得。以下示例展示了如何从 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:

Sample Person object
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

A projection interface using nullable wrappers
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:

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:

A repository using a dynamic projection parameter
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:

Using a repository with dynamic projections
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<?>

Query parameters of type Class are inspected whether they qualify as dynamic projection parameter. If the actual return type of the query equals the generic parameter type of the Class parameter, then the matching Class parameter is not available for usage within the query or SpEL expressions. If you want to use a Class parameter as query argument then make sure to use a different generic parameter, for example Class<?>.