1. 概述

在使用 Spring Data JPA 构建持久层时,通常 Repository 返回的是实体类的完整对象。但在实际开发中,我们往往并不需要对象的全部字段。

此时,如果我们能直接获取到一个定制的“部分视图”对象,就方便多了。✅Spring Data JPA 提供了 Projection(投影) 机制,允许我们只获取感兴趣的字段,而不是整个实体对象。

本文将介绍 Spring Data JPA 中几种常见的投影方式:接口投影、类投影、动态投影,以及它们的使用场景和注意事项。


2. 初始准备

2.1. Maven 依赖

本文不详细列出 Maven 依赖项,如需参考,请查看 Spring Data JPA 基础教程

2.2. 实体类定义

我们定义两个实体类:PersonAddress,它们之间是双向一对一关系:

@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
}
@Entity
public class Person {
 
    @Id
    private Long id;
 
    private String firstName;
    private String lastName;
 
    @OneToOne(mappedBy = "person")
    private Address address;

    // getters and setters
}

使用 H2 内存数据库,Spring Boot 会自动根据实体类生成对应的表结构。

2.3. SQL 脚本初始化

我们使用如下 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. 测试类

使用如下测试类验证投影行为:

@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. 接口投影(Interface-Based Projections)

接口投影是最常见、最直观的一种方式。Spring Data JPA 会为接口生成代理对象,直接返回我们关心的字段。

3.1. 封闭式投影(Closed Projections)

适用于字段名与实体类一致的场景。

public interface AddressView {
    String getZipCode();
}

在 Repository 中使用:

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

测试代码:

@Autowired
private AddressRepository addressRepository;

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

嵌套投影(Nested Projections)

还可以嵌套其他投影接口:

public interface PersonView {
    String getFirstName();
    String getLastName();
}

public interface AddressView {
    String getZipCode();
    PersonView getPerson();
}

⚠️注意:嵌套投影必须与实体类中方法名一致,且只能从“拥有方”访问“被关联方”。

@Test
public void whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned() {
    AddressView addressView = addressRepository.getAddressByState("CA").get(0);
    PersonView personView = addressView.getPerson();
    assertThat(personView.getFirstName()).isEqualTo("John");
    assertThat(personView.getLastName()).isEqualTo("Doe");
}

3.2. 开放式投影(Open Projections)

通过 @Value 注解配合 SpEL 表达式,可以自定义字段值,适用于动态计算字段。

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

在 Repository 中定义方法:

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

测试代码:

@Autowired
private PersonRepository personRepository;

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

⚠️缺点:Spring Data JPA 无法优化查询,只能加载整个实体对象再做处理,性能略差。


4. 类投影(Class-Based Projections)

除了接口投影,还可以使用自定义 DTO 类作为投影对象。

示例 DTO 类:

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
}

或者使用 Java 16+ 的 record 简化代码:

public record PersonDto(String firstName, String lastName) {}

⚠️注意:

  • 构造函数参数名必须与实体类字段名一致
  • 必须实现 equals()hashCode(),否则集合处理会出错

在 Repository 中使用:

public interface PersonRepository extends Repository<Person, Long> {
    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. 动态投影(Dynamic Projections)

当一个实体有多个投影类型时,如果为每种类型都写一个方法会很麻烦。此时可以使用动态投影:

public interface PersonRepository extends Repository<Person, Long> {
    <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. 小结

投影类型 是否支持嵌套 是否支持动态字段 性能 适用场景
接口 - 封闭式 字段与实体一致,结构简单
接口 - 开放式 需要计算字段,如 fullName
类投影 使用 DTO,便于序列化传输
动态投影 多种返回类型共用一个方法

7. 源码地址

本文完整示例代码可在 GitHub 上找到:

👉 Spring Data JPA Projections 示例代码

项目为 Maven 格式,可直接运行测试验证效果。


原始标题:Spring Data JPA Projections | Baeldung