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 {
    // ...
}

映射机制说明:

  • @SqlResultSetMappingname属性指定映射名称
  • @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只包含idtitle字段,与实体不完全匹配。然后定义自定义仓库接口:

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仓库


» 下一篇: Java Weekly, 第482期