原创

Spring Data JPA 投影

1.概述

当使用SpringDataJPA实现持久层时,存储库通常返回一个或多个实例属性。但通常我们不需要返回对象的所有属性。
在这种情况下,可能需要将数据作为自定义类型的对象来检索。这些类型反映了实体类的部分视图,而且只包含我们关心的属性。这就是投影(projections)有用的地方。

2.初始设置

新建项目并在数据库添加初始数据

2.1Maven依赖

有关依赖项,请查看本教程的第2部分(https://www.baeldung.com/spring-data-case-insensitive-queries)。

2.2.实体类

定义两个实体类:

@Entity
public class Address {

    @Id
    private Long id;

    @OneToOne
    private Person person;

    private String state;

    private String city;

    private String street;

    private String zipCode;

    // getters and setters
}
// Person实体
@Entity
public class Person {

    @Id
    private Long id;

    private String firstName;

    private String lastName;

    @OneToOne(mappedBy = "person")
    private Address address;

    // getters and setters
}

PersonAddress实体之间的关系是双向的一对一关系:Address是持有方,Person是被持有方。
注意,在本教程中,我们将使用嵌入式数据库——H2 Database。
配置完数据库后,Spring Boot会自动为我们定义的实体生成基础表。

2.3.SQL脚本

我们使用projection-insert-data.sql脚本来填充两个支持表:

INSERT INTO person(id,first_name,last_name) VALUES (1,'John','Doe');
INSERT INTO address(id,person_id,state,city,street,zip_code) 
VALUES (1,1,'CA', 'Los Angeles', 'Standford Ave', '90001');

要在每次测试运行后清理数据库,我们可以使用名为projection-clean-up-data.sql的脚本:

DELETE FROM address;
DELETE FROM person;

###2.4.测试类
为了确认projections是否产生正确的数据,我们需要一个测试类:

@DataJpaTest
@RunWith(SpringRunner.class)
@Sql(scripts = "/projection-insert-data.sql")
@Sql(scripts = "/projection-clean-up-data.sql", executionPhase = AFTER_TEST_METHOD)
public class JpaProjectionIntegrationTest {
    // injected fields and test methods
}

使用给定的注解创建数据库,注入依赖项,并在每个测试方法执行之前和之后填充和清理表。

3.基于接口的投影

在投影实体时,依赖于接口是很自然的,因为我们不需要提供实现。

3.1.封闭式投影

回顾一下Address类,我们可以看到它有很多属性,但并非所有属性都有用。例如,有时邮政编码足以表明地址。为Address类声明一个投影接口:

public interface AddressView {
    String getZipCode();
}

然后在repository接口中使用它:

public interface AddressRepository extends Repository<Address, Long> {
    List<AddressView> getAddressByState(String state);
}

很容易看出,使用投影接口定义repository方法与使用实体类几乎相同。
唯一的区别是投影接口(而不是实体类)用作返回集合中的元素类型。快速地测试下:

@Autowired
private AddressRepository addressRepository;

@Test
public void whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned() {
    AddressView addressView = addressRepository.getAddressByState("CA").get(0);
    assertThat(addressView.getZipCode()).isEqualTo("90001");
    // ...
}

在后台,Spring为每个实体对象创建投影接口的代理实例,对代理的所有调用都转发给该对象。
我们可以递归地使用投影。例如,下面是Person类的投影接口:

public interface PersonView {
String getFirstName();

String getLastName();

}
现在,让我们在Address投影中添加一个返回类型为PersonView的方法 - 一个嵌套的投影:

public interface AddressView {
// ...
PersonView getPerson();
}
请注意,返回嵌套投影的方法必须与返回相关实体的根类中的方法具有相同的名称。
让我们通过在我们刚刚编写的测试方法中添加一些语句来验证嵌套投影:

// ...
PersonView personView = addressView.getPerson();
assertThat(personView.getFirstName()).isEqualTo("John");
assertThat(personView.getLastName()).isEqualTo("Doe");
请注意,递归投影仅在我们从持有方移动到被持有方时才起作用。如果我们反过来这样做,嵌套投影将被设置为null。

3.2公开预测

到目前为止,我们已经研究了封闭投影,它指示投影接口,这些接口的方法与实体属性的名称完全匹配。
还有另一种基于界面的投影:开放投影。这些投影使我们能够用不匹配的名称和运行时计算的返回值定义接口方法。
我们回到人员投影界面,添加一个新方法:

public interface PersonView {
// ...

@Value("#{target.firstName + ' ' + target.lastName}")
String getFullName();

}
@Value注释的参数是SpEL表达式,其中目标指示符指示后备实体对象。 现在,我们将定义另一个存储库接口:

public interface PersonRepository extends Repository {
PersonView findByLastName(String lastName);
}
为简单起见,我们只返回一个投影对象而不是一个集合。 该测试确认开放预测按预期工作:

@Autowired
private PersonRepository personRepository;

@Testpublic void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
PersonView personView = personRepository.findByLastName("Doe");

assertThat(personView.getFullName()).isEqualTo("John Doe");

}
开放式预测有一个缺点:Spring Data无法优化查询执行,因为它事先不知道将使用哪些属性。因此,当封闭投影无法满足我们的要求时,我们应该只使用开放投影。

4.基于类的预测

我们可以定义自己的投影类,而不是使用Spring Data为投影界面创建的代理。 例如,这是Person实体的投影类:

public class PersonDto {
private String firstName;
private String lastName;

public PersonDto(String firstName, String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// getters, equals and hashCode

}
要使投影类与存储库接口协同工作,其构造函数的参数名称必须与根实体类的属性匹配。
我们还必须定义equals和hashCode实现 - 它们允许Spring Data处理集合中的投影对象。
现在,让我们向Person存储库添加一个方法:

public interface PersonRepository extends Repository {
// ...

PersonDto findByFirstName(String firstName);

}
此测试验证我们基于类的投影:

@Test
public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() {
PersonDto personDto = personRepository.findByFirstName("John");

assertThat(personDto.getFirstName()).isEqualTo("John");
assertThat(personDto.getLastName()).isEqualTo("Doe");

}
请注意,基于类的方法,我们不能使用嵌套投影。

5.基于类的预测

实体类可能有很多预测。在某些情况下,我们可能会使用某种类型,但在其他情况下,我们可能需要其他类型。有时,我们还需要使用实体类本身。
定义单独的存储库接口或方法只是为了支持多种返回类型是很麻烦的。为了解决这个问题,Spring Data提供了一个更好的解决方案:动态预测。
我们可以通过使用Class参数声明存储库方法来应用动态投影:

public interface PersonRepository extends Repository {
// ...

<T> T findByLastName(String lastName, Class<T> type);

}
通过将投影类型或实体类传递给这样的方法,我们可以检索所需类型的对象:

@Test
public void whenUsingDynamicProjections_thenObjectWithRequiredPropertiesIsReturned() {
Person person = personRepository.findByLastName("Doe", Person.class);
PersonView personView = personRepository.findByLastName("Doe", PersonView.class);
PersonDto personDto = personRepository.findByLastName("Doe", PersonDto.class);

assertThat(person.getFirstName()).isEqualTo("John");
assertThat(personView.getFirstName()).isEqualTo("John");
assertThat(personDto.getFirstName()).isEqualTo("John");

}

6.结论

在本文中,我们讨论了各种类型的Spring Data JPA预测。 GitHub上提供了本教程的源代码。这是一个Maven项目,应该能够按原样运行。

正文到此结束