1. 概述
本教程将探讨如何在JPA中使用Java Records。首先我们会分析为什么Records不能直接作为实体类使用,然后介绍几种在JPA中集成Records的实用方案。最后我们还会在Spring Boot应用中结合Spring Data JPA演示具体用法。
2. Records与实体的对比
Java Records是专为数据存储设计的不可变类,自动包含字段、全参构造器、getter、toString()和equals/hashCode方法。由于不可变性,它们没有setter方法。凭借简洁的语法,常被用作Java应用中的DTO(数据传输对象)。
实体类则是映射到数据库表的类,用于表示数据库中的记录。其字段与数据库表的列一一对应。
2.1. Records不能作为实体
实体由JPA提供者(如Hibernate)管理。这些提供者负责创建数据库表、映射实体与表、持久化实体到数据库。在Hibernate等主流实现中,实体通过代理类创建和管理。
这些代理类是运行时生成的实体类子类,依赖实体类必须有无参构造器和setter方法。由于Records天生缺少这些要素,因此无法直接用作实体类。
2.2. 在JPA中使用Records的其他方式
考虑到Records在Java应用中的便捷性和安全性,我们可以通过以下方式在JPA中利用它们:
- 将查询结果转换为Record对象
- 用Records作为DTO在各层间传输数据
- 将实体对象转换为Record对象
3. 项目搭建
我们将使用Spring Boot创建一个集成JPA和Spring Data JPA的简单应用,然后演示几种在数据库交互中使用Records的方式。
3.1. 依赖配置
首先添加Spring Data JPA依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.0.4</version>
</dependency>
除Spring Data JPA外,还需配置数据库。这里使用H2内存数据库作为示例。
3.2. 实体类与Record类
创建映射到数据库book
表的Book
实体类:
@Entity
@Table(name = "book")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private String isbn;
// 构造器、getter和setter(省略)
}
再创建对应的BookRecord
:
public record BookRecord(Long id, String title, String author, String isbn) {}
接下来我们将在应用中探索使用Record替代实体的几种方式。
4. 在JPA中使用Records
JPA API提供了几种数据库交互方式,其中可以集成Records。我们来看几个典型场景:
4.1. Criteria Builder方式
先看如何通过CriteriaBuilder
使用Records。以下查询返回所有图书记录:
public class QueryService {
@PersistenceContext
private EntityManager entityManager;
public List<BookRecord> findAllBooks() {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<BookRecord> query = cb.createQuery(BookRecord.class);
Root<Book> root = query.from(Book.class);
query.select(cb.construct(BookRecord.class, root.get("id"), root.get("title"), root.get("author"), root.get("isbn")));
return entityManager.createQuery(query).getResultList();
}
}
关键步骤解析:
- 通过
CriteriaBuilder.createQuery()
创建查询,传入目标Record类作为参数 - 用
CriteriaQuery.from()
指定查询的实体类(即数据库表) - 通过
CriteriaQuery.select()
指定select子句,使用CriteriaBuilder.construct()
将结果转换为Record(需传入Record类和对应字段) - 最后执行查询获取结果列表
这种方式下,查询仍基于实体类构建,但应用层使用Record对象处理结果。
4.2. Typed Query方式
类似Criteria Builder,我们也可以使用类型化查询返回Record。在QueryService
中添加按标题查询单个图书的方法:
public BookRecord findBookByTitle(String title) {
TypedQuery<BookRecord> query = entityManager
.createQuery("SELECT new com.example.records.BookRecord(b.id, b.title, b.author, b.isbn) " +
"FROM Book b WHERE b.title = :title", BookRecord.class);
query.setParameter("title", title);
return query.getSingleResult();
}
TypedQuery允许将查询结果转换为任意类型,只要该类型有与查询结果匹配的构造器参数。
4.3. 原生查询方式
原生查询也可以返回Record对象,但需要通过映射机制转换结果。首先在实体类中定义映射:
@SqlResultSetMapping(
name = "BookRecordMapping",
classes = @ConstructorResult(
targetClass = BookRecord.class,
columns = {
@ColumnResult(name = "id", type = Long.class),
@ColumnResult(name = "title", type = String.class),
@ColumnResult(name = "author", type = String.class),
@ColumnResult(name = "isbn", type = String.class)
}
)
)
@Entity
@Table(name = "book")
public class Book {
// ...
}
映射机制说明:
-
@SqlResultSetMapping
的name
属性指定映射名称 -
@ConstructorResult
声明使用Record构造器转换结果 -
targetClass
指定目标Record类 -
@ColumnResult
定义列名和类型,这些值将传递给Record构造器
然后在原生查询中使用该映射:
public List<BookRecord> findAllBooksUsingMapping() {
Query query = entityManager.createNativeQuery("SELECT * FROM book", "BookRecordMapping");
return query.getResultList();
}
5. 在Spring Data JPA中使用Records
Spring Data JPA对JPA API做了增强,提供了更便捷的Records使用方式:
5.1. 实体到Record的自动映射
Spring Data仓库接口方法可直接返回Record类型,当Record与实体字段完全匹配时会自动映射:
public interface BookRepository extends JpaRepository<Book, Long> {
List<BookRecord> findBookByAuthor(String author);
}
调用findBookByAuthor()
时,Spring Data JPA会自动将实体转换为Record并返回。
5.2. 结合@Query注解使用
类似TypedQuery,可在仓库方法中使用@Query
注解返回Record:
public interface BookRepository extends JpaRepository<Book, Long> {
@Query("SELECT new com.example.jpa.BookRecord(b.id, b.title, b.author, b.isbn) FROM Book b WHERE b.id = :id")
BookRecord findBookById(@Param("id") Long id);
}
调用findBookById()
将返回单个Record对象而非实体。
5.3. 自定义仓库实现
当自动映射不适用时,可通过自定义仓库实现灵活控制映射。先创建自定义Record类:
public record CustomBookRecord(Long id, String title) {}
注意CustomBookRecord
只包含id
和title
字段,与实体不完全匹配。然后定义自定义仓库接口:
public interface CustomBookRepository {
List<CustomBookRecord> findAllBooks();
}
实现类中使用JdbcTemplate
手动映射:
@Repository
public class CustomBookRepositoryImpl implements CustomBookRepository {
private final JdbcTemplate jdbcTemplate;
public CustomBookRepositoryImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<CustomBookRecord> findAllBooks() {
return jdbcTemplate.query("SELECT id, title FROM book",
(rs, rowNum) -> new CustomBookRecord(rs.getLong("id"), rs.getString("title")));
}
}
这里通过RowMapper
的lambda实现将结果集映射为CustomBookRecord
。
6. 将Records用作@Embeddable
根据Hibernate 6.2更新,现在支持将Java Records映射为可嵌入对象。例如用Record表示作者信息:
@Embeddable
public record Author(String firstName, String lastName) {}
在实体类中使用@Embedded
注解嵌入该Record:
@Entity
@Table(name = "embeddable_author_book")
public class EmbeddableBook {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Embedded
private Author author;
private String isbn;
//...
}
⚠️ Hibernate 6.2之前的版本需要额外配置:需添加@EmbeddableInstantiator
并实现EmbeddableInstantiator
接口:
public class AuthorInstantiator implements EmbeddableInstantiator {
@Override
public Object instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactory) {
final String firstName = valueAccess.getValue(0, String.class);
final String lastName = valueAccess.getValue(1, String.class);
return new Author(firstName, lastName);
}
// 其他接口方法实现(省略)
}
此外,Hibernate还支持通过@Struct
注解将Record映射到数据库的结构化类型(如PostgreSQL的复合类型)。
7. 总结
本文探讨了在JPA和Spring Data JPA中使用Java Records的多种方式:
✅ JPA原生方案:
- Criteria Builder动态查询
- TypedQuery类型化查询
- 原生查询+结果集映射
✅ Spring Data JPA增强方案:
- 自动实体-Record映射
- @Query注解构造查询
- 自定义仓库实现
✅ 高级用法:
- 作为@Embeddable嵌入对象
- 映射到数据库结构化类型
这些方案既保持了Records的简洁性和不可变性优势,又解决了JPA实体管理的特殊要求。实际开发中可根据场景灵活选择,在数据持久层和业务层之间建立更清晰的边界。
示例代码已上传至GitHub仓库