1.概述

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

2.初始设置

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

2.1 Maven依赖

有关依赖项,请查看本教程的第2部分。

2.2 实体类

定义两个实体类:
  1. @Entity
  2. publicclassAddress{
  3. @Id
  4. privateLong id;
  5. @OneToOne
  6. privatePerson person;
  7. privateString state;
  8. privateString city;
  9. privateString street;
  10. privateString zipCode;
  11. // getters and setters
  12. }
  13. // Person实体
  14. @Entity
  15. publicclassPerson{
  16. @Id
  17. privateLong id;
  18. privateString firstName;
  19. privateString lastName;
  20. @OneToOne(mappedBy ="person")
  21. privateAddress address;
  22. // getters and setters
  23. }
Person与 Address实体之间的关系是双向的一对一关系: Address是持有方, Person是被持有方。 注意,在本教程中,我们将使用嵌入式数据库——H2 Database。 配置完数据库后,Spring Boot会自动为我们定义的实体生成基础表。

2.3 SQL脚本

我们使用 projection-insert-data.sql脚本来填充两个支持表:
  1. INSERT INTO person(id,first_name,last_name) VALUES (1,'John','Doe');
  2. INSERT INTO address(id,person_id,state,city,street,zip_code)
  3. VALUES (1,1,'CA','Los Angeles','Standford Ave','90001');
要在每次测试运行后清理数据库,我们可以使用名为 projection-clean-up-data.sql的脚本:
  1. DELETE FROM address;
  2. DELETE FROM person;

2.4 测试类

为了确认 projections是否产生正确的数据,我们需要一个测试类:
  1. @DataJpaTest
  2. @RunWith(SpringRunner.class)
  3. @Sql(scripts ="/projection-insert-data.sql")
  4. @Sql(scripts ="/projection-clean-up-data.sql", executionPhase = AFTER_TEST_METHOD)
  5. publicclassJpaProjectionIntegrationTest{
  6. // injected fields and test methods
  7. }
使用给定的注解创建数据库,注入依赖项,并在每个测试方法执行之前和之后填充和清理表。

3.基于接口的投影

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

3.1 封闭式投影

回顾一下Address类,我们可以看到它有很多属性,但并非所有属性都有用。例如,有时邮政编码足以表明地址。为Address类声明一个投影接口:
  1. publicinterfaceAddressView{
  2. String getZipCode();
  3. }
然后在 repository接口中使用它:
  1. publicinterfaceAddressRepositoryextendsRepository<Address,Long>{
  2. List<AddressView> getAddressByState(String state);
  3. }
很容易看出,使用投影接口定义 repository方法与使用实体类几乎相同。 唯一的区别是投影接口(而不是实体类)用作返回集合中的元素类型。快速地测试下:
  1. @Autowired
  2. privateAddressRepository addressRepository;
  3. @Test
  4. publicvoid whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned(){
  5. AddressView addressView = addressRepository.getAddressByState("CA").get(0);
  6. assertThat(addressView.getZipCode()).isEqualTo("90001");
  7. // ...
  8. }
在后台,Spring为每个实体对象创建投影接口的代理实例,对代理的所有调用都转发给该对象。我们可以递归地使用投影。例如,下面是Person类的投影接口:
  1. publicinterfacePersonView{
  2. String getFirstName();
  3. String getLastName();
  4. }
现在,在 Address投影中添加一个返回类型为 PersonView的方法,也可称为嵌套投影:
  1. publicinterfaceAddressView{
  2. // ...
  3. PersonView getPerson();
  4. }
请注意,返回嵌套投影的方法必须与相关实体的主类中的方法具有相同的名称。 在刚刚编写的测试方法中来验证嵌套投影:
  1. // ...
  2. PersonView personView = addressView.getPerson();
  3. assertThat(personView.getFirstName()).isEqualTo("John");
  4. assertThat(personView.getLastName()).isEqualTo("Doe");
请注意,递归投影仅在我们从持有方移动到被持有方时才起作用。如果我们反过来这样做,嵌套投影将被设置为 null

3.2 开放式投影

到目前为止,我们已经研究了封闭式投影,投影接口的方法与实体属性的名称完全匹配。 还有另一种基于接口的投影:开放式投影。它使我们能够用不匹配的名称定义接口方法以及在运行时计算的返回值。 我们回到 Persion投影接口,添加一个新方法:
  1. publicinterfacePersonView{
  2. // ...
  3. @Value("#{target.firstName + ' ' + target.lastName}")
  4. String getFullName();
  5. }
@Value注解的参数是SpEL表达式,其中目标指示符指示支持实体对象。现在,定义另一个 repository接口:
  1. publicinterfacePersonRepositoryextendsRepository<Person,Long>{
  2. PersonView findByLastName(String lastName);
  3. }
为简单起见,我们只返回一个投影对象而不是一个集合。测试如下:
  1. @Autowired
  2. privatePersonRepository personRepository;
  3. @Testpublicvoid whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned(){
  4. PersonView personView = personRepository.findByLastName("Doe");
  5. assertThat(personView.getFullName()).isEqualTo("John Doe");
  6. }
开放式投影有一个缺点:Spring Data无法优化查询执行,因为它事先不知道将使用哪些属性。因此,当封闭式投影无法满足我们的要求时,我们才会使用开放投影。

4.基于类的投影

我们可以定义自己的投影类,而不是使用 SpringData为投影接口创建的代理。例如,这是 Person实体的投影类:
  1. publicclassPersonDto{
  2. privateString firstName;
  3. privateString lastName;
  4. publicPersonDto(String firstName,String lastName){
  5. this.firstName = firstName;
  6. this.lastName = lastName;
  7. }
  8. // getters, equals and hashCode
  9. }
要使投影类与 repository接口协同工作,其构造函数的参数名称必须与根实体类的属性匹配。我们还可以定义 equals和 hashCode实现——它们允许Spring Data处理集合中的投影对象。 现在,让我们向 Person repository接口添加一个方法:
  1. publicinterfacePersonRepositoryextendsRepository<Person,Long>{
  2. // ...
  3. PersonDto findByFirstName(String firstName);
  4. }
此测试验证我们基于类的投影:
  1. @Test
  2. publicvoid whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned(){
  3. PersonDto personDto = personRepository.findByFirstName("John");
  4. assertThat(personDto.getFirstName()).isEqualTo("John");
  5. assertThat(personDto.getLastName()).isEqualTo("Doe");
  6. }
注意,在基于类的投影方法中,不能使用嵌套投影。

5.动态投影

实体类可能有很多投影。在某些情况下,我们可能会使用某种类型,但在其他情况下,我们可能需要其他类型。有时,还需要使用实体类本身。 定义单独的存储库接口或方法只是为了支持多种返回类型是很麻烦的。为了解决这个问题,Spring Data提供了一个更好的解决方案:动态投影(dynamic projections)。 我们可以通过使用Class参数声明存储库方法来应用动态投影:
  1. publicinterfacePersonRepositoryextendsRepository<Person,Long>{
  2. // ...
  3. <T> T findByLastName(String lastName,Class<T> type);
  4. }
通过将投影类型或实体类传递给这样的方法,我们可以检索所需类型的对象:
  1. @Test
  2. publicvoid whenUsingDynamicProjections_thenObjectWithRequiredPropertiesIsReturned(){
  3. Person person = personRepository.findByLastName("Doe",Person.class);
  4. PersonView personView = personRepository.findByLastName("Doe",PersonView.class);
  5. PersonDto personDto = personRepository.findByLastName("Doe",PersonDto.class);
  6. assertThat(person.getFirstName()).isEqualTo("John");
  7. assertThat(personView.getFirstName()).isEqualTo("John");
  8. assertThat(personDto.getFirstName()).isEqualTo("John");
  9. }

6.结论

在本文中,我们讨论了各种类型的Spring Data JPA的投影。 GitHub上提供了本教程的源代码。
原文链接:https://www.baeldung.com/spring-data-jpa-projections
作者: Nguyen Nam Thai
译者:Yunooa


动手扫一扫关注,帮你不断突破技术壁垒
继续阅读
阅读原文