Spring Data JPA 中 Record 与 findAll 的兼容性问题及解决方案
2025-03-13 04:04:26
Spring Data JPA 中 Record 类与 findAll
方法的困境及对策
最近在用 SpringBoot 3.4.2 做项目,用 RESTful 控制器访问 Service,然后通过 Repository 从数据库获取数据,一切顺利。 我完全明白,大多数情况下,不应该将 Entity 直接返回给调用 RESTful API 的客户端。 有时候我们不希望某些数据被返回,有时候又需要聚合数据或者做一些计算,这时 DTO 就派上用场了。 我是完全支持 Entity 和 DTO 之间进行映射的。
最近发现 Record 这个东西,对于小型数据,可以把它当作 DTO 来用。 资料显示 Record 是不可变的。 我看了很多关于 DTO (用 Lombok 生成) 和 Record 不同的文章。 如果想保持 Repository 不变,可以自己写代码实现 Entity 到 Record 的映射,但有必要这么做吗? 从网上的例子看,可以用 Spring Data JPA 直接将数据加载到 Record 中,如果字段完全相同,映射非常简单。 如果数据有差异,也有办法解决,但我还没研究到那一步。 现在我有个小问题,可能是我遗漏了什么。 下面是我的代码:
@Entity
@Table(name = "company")
public class CompanyEntity implements Serializable
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "company_id")
private long companyId;
@Column(name = "active")
private boolean active;
@Column(name = "code")
private String companyCode;
@Column(name = "name")
private String companyName;
@Column(name = "description")
private String description;
@Column(name = "address1")
private String address1;
@Column(name = "address2")
private String address2;
@Column(name = "city")
private String city;
@Column(name = "state")
private String state;
@Column(name = "zip")
private String zip;
}
Record 类的字段和 Entity 完全一致:
public record CompanyRecord(long companyId, boolean active,
String companyCode,
String companyName, String description, String address1,
String address2, String city, String state, String zip)
implements Serializable {}
Repository 代码如下:
@Repository("CompanyRepository")
public interface CompanyRepository extends JpaRepository<CompanyEntity, Long>
{
// 工作正常
public CompanyRecord findByCompanyId(long companyId);
// 工作正常
public List<CompanyEntity> findAll();
// 这里不行!
// IDE 报错:
// The return type is incompatible with ListCrudRepository<CompanyEntity,Long>.findAll()
public List<CompanyRecord> findAll();
// 工作正常
List<CompanyRecord> findByCompanyCode(String companyCode);
}
我不确定这是不是 JpaRepository 的问题,也不知道怎么解决。 更让人困惑的是 JUnit 测试:
@Test
public void testFindById_Entity()
{
// 工作正常
// 奇怪的是,Repository 需要的是 CompanyRecord,而不是 Entity
long companyId = 1;
CompanyEntity companyEntity = companyRepository.findById(companyId).orElse(null);
assertNotNull(companyEntity);
}
@Disabled
@Test
public void testFindById_Record()
{
long companyId = 1;
// 不工作,IDE 报错
// Type mismatch: cannot convert from CompanyEntity to CompanyRecord
// 我以为能正常工作,因为接口明确要求返回 Record
CompanyRecord companyRecord = companyRepository.findById(companyId).orElse(null);
assertNotNull(companyRecord);
}
@Test
public void testFindByCode_Entity()
{
String companyCode = "IBM";
// 不工作,符合预期,因为接口需要 Record
List<CompanyEntity> companyEntityList = companyRepository.findByCompanyCode(companyCode);
assertNotNull(companyEntityList);
}
@Test
public void testFindByCode()
{
String companyCode = "IBM";
// 工作正常,接口需要 Record
List<CompanyRecord> companyRecordList = companyRepository.findByCompanyCode(companyCode);
assertNotNull(companyRecordList);
}
@Test
public void testFindAll_Entity()
{
// 工作正常
List<CompanyEntity> companyEntityList = companyRepository.findAll();
assertNotNull(companyEntityList);
}
这样用下来,感觉很混乱。 有时候符合预期,有时候又不行。 感觉 Record 还没完全准备好。 或者,我可以退回到老办法,在 Repository 里只用 Entity,然后在 Service 里手动创建 Record,但这样用 Spring Data JPA 里的 Record 就能减少很多重复代码。
大家有没有类似的经验,或者对 Java Record 在 Spring Data JPA 中的应用有更深入的了解?
问题分析:为何 findAll
不能直接返回 List<CompanyRecord>
?
问题的核心在于 JpaRepository
的 findAll()
方法的定义。JpaRepository
继承自 ListCrudRepository
,而 ListCrudRepository
的 findAll()
方法返回的是泛型 T
的列表,也就是 List<T>
。 在你的例子里,T
被指定为 CompanyEntity
,所以 findAll()
只能返回 List<CompanyEntity>
。
直接在接口里声明 List<CompanyRecord> findAll()
是不行的,因为这违反了 JpaRepository
的约定。
另外,在测试用例testFindById_Entity
能够通过是因为findById
方法返回的是Optional<CompanyEntity>
类型。虽然CompanyRepository
接口中的findByCompanyId
指定的是CompanyRecord
返回类型,但JpaRepository
中findById
实际返回的类型是Optional,其包含的对象是实体类。
解决方案
要让 findAll
返回 List<CompanyRecord>
,有以下几种方法:
1. 使用 JPQL (Java Persistence Query Language)
在 Repository 接口中,用 @Query
注解自定义查询,将查询结果映射到 CompanyRecord
。
@Repository("CompanyRepository")
public interface CompanyRepository extends JpaRepository<CompanyEntity, Long>
{
// ... 其他方法 ...
@Query("SELECT new com.example.yourpackage.CompanyRecord(c.companyId, c.active, c.companyCode, c.companyName, c.description, c.address1, c.address2, c.city, c.state, c.zip) FROM CompanyEntity c")
List<CompanyRecord> findAllCompanyRecords();
}
原理:
- JPQL 允许你直接在查询中构造对象。
new com.example.yourpackage.CompanyRecord(...)
会调用CompanyRecord
的构造函数,将查询结果的每一列映射到构造函数的参数。- 确保
com.example.yourpackage
替换成你的CompanyRecord
所在的包名。
代码示例: (已经在上面提供)
注意点: 如果不采用别名c
进行属性访问,则需要在CompanyRecord
中定义匹配数据库字段名字的构造器,如: @Query("SELECT new com.example.yourpackage.CompanyRecord(c.company_id, ...)
。
进阶使用技巧:
可以用JPQL来做一些比较复杂的查询,比如关联查询,条件查询,等。这样你的查询会更加的灵活。
2. 使用原生 SQL (Native SQL)
与 JPQL 类似,但使用数据库的原生 SQL 语句。
@Repository("CompanyRepository")
public interface CompanyRepository extends JpaRepository<CompanyEntity, Long>
{
// ... 其他方法 ...
@Query(value = "SELECT company_id, active, code AS companyCode, name AS companyName, description, address1, address2, city, state, zip FROM company", nativeQuery = true)
List<CompanyRecord> findAllCompanyRecords();
}
原理:
nativeQuery = true
告诉 Spring Data JPA 使用原生 SQL。AS
用来给查询结果的列起别名,使其与CompanyRecord
的字段名匹配。- 因为是使用原生SQL,可以无视数据库字段与实体类中字段的命名差异。
代码示例: (已经在上面提供)
安全性建议:
- 使用原生 SQL 时,要小心 SQL 注入攻击。 尽量使用参数化查询。
- 原生 SQL 可能导致代码与特定数据库绑定,降低可移植性。
进阶使用技巧
这种查询可以无视JPA和实体的要求和限制, 如果你需要非常高效或者数据库特定的查询,考虑这个方案。
3. 在 Service 层进行转换
这是最灵活的方式,也是最推荐的方式。
@Service
public class CompanyService {
@Autowired
private CompanyRepository companyRepository;
public List<CompanyRecord> getAllCompanies() {
List<CompanyEntity> entities = companyRepository.findAll();
return entities.stream()
.map(entity -> new CompanyRecord(entity.getCompanyId(), entity.isActive(), entity.getCompanyCode(),
entity.getCompanyName(), entity.getDescription(), entity.getAddress1(), entity.getAddress2(),
entity.getCity(), entity.getState(), entity.getZip()))
.toList();
}
}
原理:
- 在 Service 层调用
companyRepository.findAll()
获取List<CompanyEntity>
。 - 使用 Java Stream API 将
List<CompanyEntity>
转换为List<CompanyRecord>
。
代码示例: (已经在上面提供)
好处:
- 将数据转换逻辑从 Repository 移到 Service,职责更清晰。
- 可以灵活地处理各种映射需求,包括复杂的计算和数据转换。
- 保持了Repository的简洁。
进阶使用技巧: 可以使用一些工具来处理映射过程, 比如 ModelMapper 和 MapStruct, 如果你有大量这种转换需求, 他们可以给你带来很多便利。
4. 使用自定义结果转换器 (ResultTransformer) (不推荐,更复杂)
可以实现org.hibernate.transform.ResultTransformer
来完成这种转换.
原理:
可以在 Hibernate 级别拦截查询结果并进行转换. 通常这适用于更复杂的映射场景,需要自定义转换逻辑时。 你的场景里并不需要这样做.
因为这种做法比较进阶且容易产生不必要代码,所以代码实现略过。有需要的可以自行研究。
总结
总的来说,处理 Spring Data JPA 和 Record 之间的映射问题,推荐在 Service 层进行转换。这种方式既保持了Repository的简洁,也让代码拥有最好的灵活性和可维护性。 如果希望在 Repository 层解决,JPQL 是一个相对简洁的选择。 原生 SQL 提供了最大的灵活性,但要注意安全性和可移植性问题。