1. 概述
在本教程中,我们将探讨如何将Java Record与JPA结合使用。我们将首先探讨为什么不能在实体中使用记录。
然后,我们将了解如何将记录与JPA一起使用。我们还将了解如何在Spring Boot应用程序中将记录与Spring Data JPA一起使用。
2. 记录与实体
记录是不可变的,用于存储数据。它们包含字段、全参数构造函数、getter、toString和equals/hashCode方法。由于它们是不可变的,因此它们没有setter方法。由于其简洁的语法,它们通常用作Java应用程序中的数据传输对象(DTO)。
实体是映射到数据库表的类。它们用于表示数据库中的条目。它们的字段映射到数据库表中的列。
2.1 记录不能是实体
实体由JPA提供程序处理。JPA提供程序负责创建数据库表,将实体映射到表,并将实体持久保存到数据库。在Hibernate等流行的JPA提供程序中,实体是使用代理创建和管理的。
代理是在运行时生成并扩展实体类的类。这些代理依赖于实体类具有无参数构造函数和setters。由于记录没有这些,因此它们不能用作实体。
2.2 在JPA中使用记录的其他方式
由于在Java应用程序中使用记录的简便性和安全性,因此以其他方式将它们与JPA一起使用可能是有益的。
在JPA中,我们可以通过以下方式使用记录:
- 将查询结果转换为记录
- 使用记录作为DTO在层之间传输数据
- 将实体转换为记录
3. 项目设置
我们将使用Spring Boot创建一个使用JPA和Spring Data JPA的简单应用程序。然后我们将看看在与数据库交互时使用记录的几种方法。
3.1 Maven依赖
让我们首先将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 实体和记录
让我们创建一个用于与数据库交互的实体。我们将创建一个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实体的记录:
public record BookRecord(Long id, String title, String author, String isbn) {
}
接下来,我们将介绍几种在应用程序中使用记录而不是实体的方法。
4. 在JPA中使用记录
JPA API提供了几种与可以使用记录的数据库进行交互的方法。
4.1 CriteriaBuilder
让我们首先看看如何通过CriteriaBuilder使用记录。我们将进行一个查询,返回数据库中的所有书籍:
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创建一个返回BookRecord的CriteriaQuery。
让我们看一下上面代码中的一些步骤:
- 我们使用CriteriaBuilder.createQuery()方法创建一个CriteriaQuery。我们将要返回的记录的类作为参数传递
- 然后我们使用CriteriaQuery.from()方法创建一个Root。我们将实体类作为参数传递。这就是我们指定要查询的表的方式
- 然后,我们使用CriteriaQuery.select()方法来指定一个select子句。我们使用CriteriaBuilder.construct()方法将查询结果转换为记录。我们将记录的Class和我们要传递给记录构造函数的实体的字段作为参数
- 最后,我们使用EntityManager.createQuery()方法从CriteriaQuery创建一个TypedQuery。然后我们使用TypedQuery.getResultList()方法来获取查询的结果
这将创建一个选select查询来获取数据库中的所有书籍。然后它将使用construct()方法将每个结果转换为BookRecord,并在我们调用getResultList()方法时返回记录列表而不是实体列表。
这样,我们可以使用实体类来创建查询,而对应用程序的其余部分使用记录。
4.2 TypedQuery
与CriteriaBuilder类似,我们可以使用类型化查询来返回记录而不是实体。让我们在QueryService中添加一个方法,以使用类型化查询获取单本书作为记录:
public BookRecord findBookById(Long id) {
TypedQuery<BookRecord> query = entityManager
.createQuery("SELECT new cn.tuyucheng.taketoday.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。我们将查询字符串和记录的Class作为参数传递。然后,我们使用TypedQuery.setParameter()方法来设置查询的参数。最后,我们使用TypedQuery.getSingleResult()方法获取查询结果,这将是一个BookRecord对象。
4.3 原生查询
我们还可以使用原生查询来获取查询结果作为记录。但是,原生查询不允许我们将结果转换为任何类型。相反,我们需要使用映射将结果转换为记录。首先,让我们在实体中定义一个映射:
@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注解指定我们要使用记录的构造函数来转换结果
- @ConstructorResult注解的targetClass属性指定记录的类
- @ColumnResult注解指定列名和列的类型。这些列值将传递给记录的构造函数
然后,我们可以在原生查询中使用此映射来将结果作为记录获取:
public List<BookRecord> findAllBooksUsingMapping() {
Query query = entityManager.createNativeQuery("SELECT * FROM book", "BookRecordMapping");
return query.getResultList();
}
这将创建一个返回数据库中所有书籍的原生查询。当我们调用getResultList()方法时,它将使用映射将结果转换为BookRecord并返回记录列表而不是实体列表。
5. 将记录与Spring Data JPA结合使用
Spring Data JPA对JPA API进行了一些改进,它使我们能够以几种方式将记录与Spring Data JPA Repository一起使用。
5.1 从实体到记录的自动映射
Spring Data Repository允许我们使用记录作为Repository中方法的返回类型,这将自动将实体映射到记录。这只有在记录具有与实体完全相同的字段时才有可能。让我们看一个例子:
public interface BookRepository extends JpaRepository<Book, Long> {
List<BookRecord> findBookByAuthor(String author);
}
由于BookRecord与Book实体具有相同的字段,因此当我们调用findBookByAuthor()方法时,Spring Data JPA会自动将实体映射到记录并返回记录列表而不是实体列表。
5.2 将记录与@Query一起使用
与TypedQuery类似,我们可以在Spring Data JPA Repository中使用带有@Query注解的记录。让我们看一个例子:
public interface BookRepository extends JpaRepository<Book, Long> {
@Query("SELECT new cn.tuyucheng.taketoday.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并返回单个记录而不是实体。
5.3 自定义Repository实现
如果自动映射不是一种选择,我们还可以定义一个自定义Repository实现,允许我们定义自己的映射。让我们首先创建一个CustomBookRecord类,该类将用作Repository中方法的返回类型:
public record CustomBookRecord(Long id, String title) {
}
请注意,CustomBookRecord类没有与Book实体相同的字段,它只有id和title字段。
然后,我们可以创建一个将使用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()方法执行查询,并使用Lambda表达式将结果映射到CustomBookRecord中,该表达式是RowMapper接口的实现。
6. 总结
在本文中,我们研究了如何将记录与JPA和Spring Data JPA一起使用。我们介绍了如何使用CriteriaBuilder、TypedQuery和原生查询将记录与JPA API一起使用。我们还了解了如何使用自动映射、自定义查询和自定义Repository实现将记录与Spring Data JPA Repository一起使用。
与往常一样,本教程的完整源代码可在GitHub上获得。