Spring Data JPA Projections – Spring Data JPA预测

最后修改: 2019年 4月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

When using Spring Data JPA to implement the persistence layer, the repository typically returns one or more instances of the root class. However, more often than not, we don’t need all the properties of the returned objects.

当使用Spring Data JPA 来实现持久层时,存储库通常会返回根类的一个或多个实例。然而,更多时候,我们并不需要返回对象的所有属性。

In such cases, we might want to retrieve data as objects of customized types. These types reflect partial views of the root class, containing only the properties we care about. This is where projections come in handy.

在这种情况下,我们可能希望以自定义类型的对象来检索数据。这些类型反映了根类的部分视图,只包含我们关心的属性。这就是投影的用武之地。

2. Initial Setup

2.初始设置

The first step is to set up the project and populate the database.

第一步是建立项目并填充数据库。

2.1. Maven Dependencies

2.1.Maven的依赖性

For dependencies, please check out section 2 of this tutorial.

关于依赖性,请查看本教程的第2节。

2.2. Entity Classes

2.2.实体类[/strong]

Let’s define two entity classes:

让我们定义两个实体类。

@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
}

And:

还有。

@Entity
public class Person {
 
    @Id
    private Long id;
 
    private String firstName;
 
    private String lastName;
 
    @OneToOne(mappedBy = "person")
    private Address address;

    // getters and setters
}

The relationship between Person and Address entities is bidirectional one-to-one; Address is the owning side, and Person is the inverse side.

PersonAddress实体之间的关系是双向的一对一;Address是拥有方,而Person是反方。

Notice in this tutorial, we use an embedded database, H2.

注意在本教程中,我们使用了一个嵌入式数据库H2。

When an embedded database is configured, Spring Boot automatically generates underlying tables for the entities we defined.

当配置了嵌入式数据库后,Spring Boot会自动为我们定义的实体生成底层表格。

2.3. SQL Scripts

2.3.SQL脚本

We’ll use the projection-insert-data.sql script to populate both the backing tables:

我们将使用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');

To clean up the database after each test run, we can use another script, projection-clean-up-data.sql:

为了在每次测试运行后清理数据库,我们可以使用另一个脚本,projection-clean-up-data.sql

DELETE FROM address;
DELETE FROM person;

2.4. Test Class

2.4.测试类

Then, to confirm the projections produce the correct data, we need a test class:

然后,为了确认投影产生正确的数据,我们需要一个测试类。

@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
}

With the given annotations, Spring Boot creates the database, injects dependencies, and populates and cleans up tables before and after each test method’s execution.

通过给定的注解,Spring Boot创建了数据库,注入了依赖关系,并在每个测试方法的执行前后填充和清理了表。

3. Interface-Based Projections

3.基于界面的预测

When projecting an entity, it’s natural to rely on an interface, as we won’t need to provide an implementation.

当投射一个实体时,自然要依靠一个接口,因为我们不需要提供一个实现。

3.1. Closed Projections

3.1.封闭式预测

Looking back at the Address class, we can see it has many properties, yet not all of them are helpful. For example, sometimes a zip code is enough to indicate an address.

回顾一下Address类,我们可以看到它有很多属性,然而并不是所有的属性都是有用的。例如,有时一个邮政编码就足以表明一个地址。

Let’s declare a projection interface for the Address class:

让我们为Address类声明一个投影接口。

public interface AddressView {
    String getZipCode();
}

Then we’ll use it in a repository interface:

然后我们将在存储库接口中使用它。

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

It’s easy to see that defining a repository method with a projection interface is pretty much the same as with an entity class.

很容易看出,用投影接口定义存储库方法和用实体类定义存储库方法是差不多的。

The only difference is that the projection interface, rather than the entity class, is used as the element type in the returned collection.

唯一的区别是,投影接口,而不是实体类,被用作返回集合中的元素类型。

Let’s do a quick test of the Address projection:

让我们对Address投影做一个快速测试。

@Autowired
private AddressRepository addressRepository;

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

Behind the scenes, Spring creates a proxy instance of the projection interface for each entity object, and all calls to the proxy are forwarded to that object.

在幕后,Spring为每个实体对象创建了一个投影接口的代理实例,所有对代理的调用都被转发给该对象。

We can use projections recursively. For instance, here’s a projection interface for the Person class:

我们可以递归地使用投影。例如,这里有一个用于Person类的投影接口。

public interface PersonView {
    String getFirstName();

    String getLastName();
}

Now we’ll add a method with the return type PersonView, a nested projection, in the Address projection:

现在我们将在Address投影中添加一个返回类型为PersonView的方法,这是一个嵌套投影。

public interface AddressView {
    // ...
    PersonView getPerson();
}

Notice the method that returns the nested projection must have the same name as the method in the root class that returns the related entity.

注意,返回嵌套投影的方法必须与根类中返回相关实体的方法名称相同。

We’ll verify nested projections by adding a few statements to the test method we’ve just written:

我们将通过在我们刚刚写的测试方法中添加一些语句来验证嵌套的投影。

// ...
PersonView personView = addressView.getPerson();
assertThat(personView.getFirstName()).isEqualTo("John");
assertThat(personView.getLastName()).isEqualTo("Doe");

Note that recursive projections only work if we traverse from the owning side to the inverse side. If we do it the other way around, the nested projection would be set to null.

请注意,递归投影只有在我们从拥有方遍历到反方时才会起作用。如果我们反过来做,嵌套投影将被设置为null

3.2. Open Projections

3.2.开放式预测

Up to this point, we’ve gone through closed projections, which indicate projection interfaces whose methods exactly match the names of entity properties.

到此为止,我们已经经历了封闭式投影,这表明投影接口的方法与实体属性的名称完全匹配。

There’s also another sort of interface-based projection, open projections. These projections enable us to define interface methods with unmatched names and with return values computed at runtime.

还有另一种基于接口的投射,即开放投射。这些投射使我们能够定义名称不匹配的接口方法,并且在运行时计算返回值。

Let’s go back to the Person projection interface and add a new method:

让我们回到Person投影接口,添加一个新方法。

public interface PersonView {
    // ...

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

The argument to the @Value annotation is a SpEL expression, in which the target designator indicates the backing entity object.

@Value注解的参数是一个SpEL表达式,其中target指定器表示支持实体对象。

Now we’ll define another repository interface:

现在我们要定义另一个资源库接口。

public interface PersonRepository extends Repository<Person, Long> {
    PersonView findByLastName(String lastName);
}

To make it simple, we’ll only return a single projection object instead of a collection.

为了简单起见,我们将只返回一个投影对象,而不是一个集合。

This test confirms the open projections work as expected:

这个测试证实了开放性预测的工作符合预期。

@Autowired
private PersonRepository personRepository;

@Test 
public void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() {
    PersonView personView = personRepository.findByLastName("Doe");
 
    assertThat(personView.getFullName()).isEqualTo("John Doe");
}

Open projections do have a drawback though; Spring Data can’t optimize query execution, as it doesn’t know in advance which properties will be used. Thus, we should only use open projections when closed projections aren’t capable of handling our requirements.

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

4. Class-Based Projections

4.基于班级的预测

Instead of using proxies Spring Data creates from projection interfaces, we can define our own projection classes.

与其使用Spring Data从投影接口创建的代理,我们可以定义我们自己的投影类。

For example, here’s a projection class for the Person entity:

例如,这里有一个用于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
}

For a projection class to work in tandem with a repository interface, the parameter names of its constructor must match the properties of the root entity class.

为了让投影类与资源库接口协同工作,其构造函数的参数名称必须与根实体类的属性相匹配。

We must also define equals and hashCode implementations; they allow Spring Data to process projection objects in a collection.

我们还必须定义equalshashCode实现;它们允许Spring Data处理集合中的投影对象。

Now let’s add a method to the Person repository:

现在让我们给Person资源库添加一个方法。

public interface PersonRepository extends Repository<Person, Long> {
    // ...

    PersonDto findByFirstName(String firstName);
}

This test verifies our class-based projection:

这个测试验证了我们基于类的推算。

@Test
public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() {
    PersonDto personDto = personRepository.findByFirstName("John");
 
    assertThat(personDto.getFirstName()).isEqualTo("John");
    assertThat(personDto.getLastName()).isEqualTo("Doe");
}

Notice with the class-based approach, we can’t use nested projections.

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

5. Dynamic Projections

5.动态预测

An entity class may have many projections. In some cases, we may use a certain type, but in other cases, we may need another type. Sometimes, we also need to use the entity class itself.

一个实体类可能有许多投影。在某些情况下,我们可能使用某种类型,但在其他情况下,我们可能需要另一种类型。有时,我们也需要使用实体类本身。

Defining separate repository interfaces or methods just to support multiple return types is cumbersome. To deal with this problem, Spring Data provides a better solution, dynamic projections.

仅仅为了支持多种返回类型而定义单独的资源库接口或方法是很麻烦的。为了处理这个问题,Spring Data提供了一个更好的解决方案,即动态投影。

We can apply dynamic projections just by declaring a repository method with a Class parameter:

我们只需声明一个带有Class参数的存储库方法就可以应用动态投影:

public interface PersonRepository extends Repository<Person, Long> {
    // ...

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

By passing a projection type or the entity class to such a method, we can retrieve an object of the desired 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. Conclusion

6.结论

In this article, we discussed various types of Spring Data JPA projections.

在这篇文章中,我们讨论了各种类型的Spring Data JPA投射。

The source code for this article is available over on GitHub. This is a Maven project and should be able to run as-is.

本文的源代码可在GitHub上找到。这是一个Maven项目,应该可以按原样运行。