1. 概述
在使用 Spring Data JPA 构建持久层时,通常 Repository 返回的是实体类的完整对象。但在实际开发中,我们往往并不需要对象的全部字段。
此时,如果我们能直接获取到一个定制的“部分视图”对象,就方便多了。✅Spring Data JPA 提供了 Projection(投影) 机制,允许我们只获取感兴趣的字段,而不是整个实体对象。
本文将介绍 Spring Data JPA 中几种常见的投影方式:接口投影、类投影、动态投影,以及它们的使用场景和注意事项。
2. 初始准备
2.1. Maven 依赖
本文不详细列出 Maven 依赖项,如需参考,请查看 Spring Data JPA 基础教程。
2.2. 实体类定义
我们定义两个实体类:Person
和 Address
,它们之间是双向一对一关系:
@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 格式,可直接运行测试验证效果。