在 JPA 和 Sping Data JPA 中使用 Java Record

1、概览

在本教程中,我们将探讨如何在 JPA 中使用 Java Record,包括以下内容。

  1. 为什么 Record 不能作为 Entity 使用。
  2. 在 JPA 中使用 Record。
  3. 在 Spring Boot 应用中使用 Spring Data JPA 和 Record。

2、Record 和 Enttiy

Record 是不可变的,用于存储数据。它们包含字段、全参数构造函数、getter、toStringequals/hashCode 方法。由于它们是不可变的,因此没有 setter。由于其语法简洁,在 Java 中经常被用作数据传输对象(DTO)。

Entity(实体)是映射到数据库表的类。它们用于表示数据库中的条目。它们的字段被映射到数据库表中的列。

2.1、Record 不能作为 Entity

实体由 JPA provider 处理。JPA provider 负责创建数据库表,将实体映射到表,并将实体持久化到数据库。在流行的 JPA provider(如 Hibernate)中,实体是使用代理来创建和管理的。

代理是在运行时生成并继承实体类的类。这些代理依赖于实体类的无参数构造函数和 setter。由于 Record 不具有这些,所以它们不能用作实体。

2.2、在 JPA 中使用 Record 的其他方法

由于在 Java 中使用 Record 的简便性和安全性,在 JPA 中以其他方式使用 Record 可能是有益的。

在 JPA中,我们可以通过以下方式使用 Record:

  • 将查询结果转换为 Record。
  • 使用 Record 作为DTO在各个层之间传输数据。
  • 将实体转换为 Record。

3、项目设置

我们将使用 Spring Boot 创建一个使用 JPA 和 Spring Data JPA 的简单应用程序。然后,我们将了解在与数据库交互时使用 Record 的几种方法。

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,我们还需要配置一个数据库。我们可以使用任何SQL数据库。例如,我们可以使用H2内存数据库。

3.2、Entity 和 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;
    
    // constructors, getters, setters
}

创建一个与 Book 实体相对应的 Record:

public record BookRecord(Long id, String title, String author, String isbn) {}

接下来,我们将了解在应用中使用 Record 而不是实体的几种方法。

4、在 JPA 中使用 Record

JPA API 提供了一些与数据库交互的方法,在这些方法中可以使用 Record。让我们来看看其中的几种。

4.1、Criteria Builder

先来看看如何在 CriteriaBuilder 中使用 Record。创建一个查询,返回数据库中的所有 book:

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 创建一个 CriteriaQuery,返回一个 BookRecord

让我们来看看上述代码中的一些步骤:

  • 使用 CriteriaBuilder.createQuery() 方法创建 CriteriaQuery。将想要返回的 record 类作为参数传递给 CriteriaBuilder.createQuery() 方法。
  • 然后,使用 CriteriaQuery.from() 方法创建一个 Root。将实体类作为参数传递。这就是我们指定要查询的表的方法。
  • 然后,使用 CriteriaQuery.select() 方法指定一个 select 子句。我们使用 CriteriaBuilder.construct() 方法将查询结果转换为 record。将 record 的类和实体的字段作为参数传递给记录构造函数
  • 最后,使用 EntityManager.createQuery() 方法从 CriteriaQuery 创建一个 TypedQuery。然后,使用 TypedQuery.getResultList() 方法获取查询结果。

这将创建一个 select 查询来获取数据库中的所有 book。然后使用 construct() 方法将每个结果转换为 BookRecord,并在调用 getResultList() 方法时返回 Record List 而不是实体 List。

通过这种方式,我们可以使用实体类创建查询,但在应用程序的其余部分使用 Record。

4.2、Typed Query

CriteriaBuilder 类似,我们可以使用类型查询来返回 record 而不是实体。在 QueryService 中添加一个方法,使用类型查询获取单条 book 记录:

public BookRecord findBookById(Long id) {
    TypedQuery<BookRecord> query = entityManager
      .createQuery("SELECT new com.baeldung.jpa.records.BookRecord(b.id, b.title, b.author, b.isbn) FROM Book b WHERE b.id = :id",
        BookRecord.class);
    query.setParameter("id", id);
    return query.getSingleResult();
}

TypedQuery 允许将查询结果转换为任何类型,只要该类型有一个构造函数,且该构造函数的参数数量与查询结果相同。

在上面的代码中,使用 EntityManager.createQuery() 方法创建一个 TypedQuery。传递查询字符串和 record 类作为参数。然后,使用 TypedQuery.setParameter() 方法设置查询参数。最后,使用 TypedQuery.getSingleResult() 方法获取查询结果,该结果将是一个 BookRecord 对象。

4.3、原生查询

我们也可以使用原生查询来获取查询结果,封装为 record。然而,原生查询不允许我们将结果转换为任何类型。相反,我们需要使用映射将结果转换为 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 的构造函数来转换结果。
  • @ConstructorResult 注解的 targetClass 属性指定了 record 类。
  • @ColumnResult 注解指定了列的名称和类型。这些列值将传递给 record 的构造函数。

然后,我们可以在原生查询中使用该映射,以 record 形式获取结果:

public List<BookRecord> findAllBooksUsingMapping() {
    Query query = entityManager.createNativeQuery("SELECT * FROM book", "BookRecordMapping");
    return query.getResultList();
}

这将创建一个原生查询,返回数据库中的所有 book 记录。当调用 getResultList() 方法时,它将使用映射将结果转换为 BookRecord,并返回 record List 而不是实体 List。

5、在 Spring Data JPA 中使用 Record

Spring Data JPA 为 JPA API 提供了一些改进。它使我们能够以几种方式在 Spring Data JPA Repository 中使用 Record。让我们看看如何在 Spring Data JPA Repository 中使用 Record。

5.1、自动映射实体到Record

Spring Data Repository 允许使用 Record 作为存 Repository 中方法的返回类型。这将自动映射实体到 record。这只有在 record 的字段与实体完全相同时才可能实现。让我们来看一个示例:

public interface BookRepository extends JpaRepository<Book, Long> {
    List<BookRecord> findBookByAuthor(String author);
}

由于 BookRecordBook 实体具有相同的字段,所以当我们调用 findBookByAuthor() 方法时,Spring Data JPA 将自动映射实体到 record,并返回 record List 而不是实体 List。

5.2、在 @Query 中使用 Record

TypedQuery 类似,可以在 Spring Data JPA Repository 的 @Query 查询方法中使用 Record。示例如下:

public interface BookRepository extends JpaRepository<Book, Long> {
    @Query("SELECT new com.baeldung.jpa.records.BookRecord(b.id, b.title, b.author, b.isbn) FROM Book b WHERE b.id = :id")
    BookRecord findBookById(@Param("id") Long id);
}

当调用 findBookById() 方法时,Spring Data JPA 将自动把查询结果转换成 BookRecord,并返回 record 而不是实体。

5.3、自定义 Repository 实现

如果不能选择自动映射,我们也可以定义一个自定义 repository 实现,以定义自己的映射。让我们从创建一个 CustomBookRecord 类开始,该类将作为 repository 中方法的返回类型:

public record CustomBookRecord(Long id, String title) {}

注意,CustomBookRecord 类没有与 Book 实体的字段完全相同。它只有 idtitle 字段。

然后,可以创建一个使用 CustomBookRecord 类的自定义 repository 实现:

public interface CustomBookRepository {
    List<CustomBookRecord> findAllBooks();
}

在 repository 的实现中,我们可以定义用于将查询结果映射到 CustomBookRecord 类的方法:

@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")));
    }
}

在上面的代码中,使用 JdbcTemplate.query() 方法来执行查询,并使用 RowMapper 接口实现的 lambda 表达式将结果映射到 CustomBookRecord 中。

6、总结

在本文中,我们了解了如何在 JPA 和 Spring Data JPA 中使用 Record。以及如何在 CriteriaBuilderTypedQuery 和原生查询的 JPA API 中使用 Record。我们还了解了如何使用自动映射、自定义查询和自定义 repository 实现在Spring Data JPA repository 中使用 Record。


参考: https://www.baeldung.com/spring-jpa-java-records