作者简介
陈喆,现就职于中科院某研究所担任副研究员,专注于工业云平台、MES系统的设计与研发。
内容来源:https://docs.spring.io/spring-data/jpa/docs/2.0.9.RELEASE/reference/html/#projections
Spring Data的查询方法可以返回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 Data实现只获取person的name属性。
基于接口的投影
限制只查询name属性最简单的方法就是声明一个接口只暴露需要读取的属性,如下所示:
interface NamesOnly {
String getFirstname();
String getLastname();
}
注意这里定义的属性需要与Person中的属性一致。然后添加查询方法:
interface PersonRepository extends Repository<Person, UUID> {
Collection<NamesOnly> findByLastname(String lastname);
}
查询执行引擎会在运行时为每一个返回元素的接口实例创建一个代理然后再将函数调用传递给目标对象暴露的方法。
投影可以递归调用。如果你还想包含一些Address信息,创建一个投影并返回一个声明自getAddress()的接口:
interface PersonSummary {
String getFirstname();
String getLastname();
AddressSummary getAddress();
interface AddressSummary {
String getCity();
}
}
一旦方法被调用,将获取目标实例的address属性并依次封装到投影代理中。
封闭投影
如果一个投影接口的存取器方法与其目标聚合的属性完全一致,则认为该投影为封闭投影。下例是一个封闭投影:
interface NamesOnly {
String getFirstname();
String getLastname();
}
如果你使用封闭投影,Spring Data会优化查询执行。
开放投影
投影接口的存取器可以通过@Value注解计算一个新的值,如下例:
interface NamesOnly {
@Value("#{target.firstname + ' ' + target.lastname}")
String getFullName();
…
}
投影对应的聚合对应target变量。使用@Value的投影就是开放投影。Spring Data无法对开放投影进行优化。
@Value中的表达式不能太过复杂-尽量避免使用字符串变量编程。对于特别简单的表达式,可以使用默认方法(使用default关键字),如下例:
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数组表示。如下例:
interface NamesOnly {
@Value("#{args[0] + ' ' + target.firstname + '!'}")
String getSalutation(String prefix);
}
基于类的投影(DTOs)
另一个定义投影的方法是使用包含待查询字段的值类型DTOs(Data Transfer Objects)。这些DTO类型和投影接口的用法相同,但没有代理,也不能做内嵌投影。
如果需要通过限制查询字段来优化查询效率,加载哪个字段则通过构造函数的参数决定。
下例是一个投影的DTO:
class NamesOnly {
private final String firstname, lastname;
NamesOnly(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
String getFirstname() {
return this.firstname;
}
String getLastname() {
return this.lastname;
}
// equals(…) and hashCode() implementations
}
可以通过使用Project Lombok的@Value注解来动态简化DTO的代码。如下面的代码:
@Value
class NamesOnly {
String firstname, lastname;
}
类中字段默认为private final,并且类会暴露一个包含所有字段的构造函数,并自动实现equals()和hashCode()函数。
动态投影
上面的例子中,我们将投影类型作为返回值类型或集合的元素类型。然后,你可能想要在调用的时候选择类型。下例演示如何使用动态投影:
interface PersonRepository extends Repository<Person, UUID> {
<T> Collection<T> findByLastname(String lastname, Class<T> type);
}
下例演示如何调用动态投影:
void someMethod(PersonRepository people) {
Collection<Person> aggregates =
people.findByLastname("Matthews", Person.class);
Collection<NamesOnly> aggregates =
people.findByLastname("Matthews", NamesOnly.class);
}