1. Overview
In this tutorial, we’ll explore how to use Java Records with JPA. We’ll start by exploring why records can’t be used at entities.
Then, we’ll see how to use records with JPA. We’ll also look at how to use records with Spring Data JPA in a Spring Boot Application.
2. Records vs. Entities
Records are immutable and are used to store data. They contain fields, all-args constructor, getters, toString, and equals/hashCode methods. Since they are immutable, they don’t have setters. Because of their concise syntax, they are often used as data transfer objects (DTOs) in Java applications.
Entities are classes that are mapped to a database table. They are used to represent an entry in a database. Their fields are mapped to columns in the database table.
2.1. Records Can’t Be Entities
Entities are handled by the JPA provider. JPA providers are responsible for creating the database tables, mapping the entities to the tables, and persisting the entities to the database. In popular JPA providers like Hibernate, entities are created and managed using proxies.
Proxies are classes that are generated at runtime and extend the entity class. These proxies rely on the entity class to have a no-args constructor and setters. Since records don’t have these, they can’t be used as entities.
2.2. Other Ways to Use Records with JPA
Due to the ease and safety of using records within Java applications, it may be beneficial to use them with JPA in some other ways.
In JPA, we can use records in the following ways:
- Convert the results of a query to a record
- Use records as DTOs to transfer data between layers
- Convert entities to records.
3. Project Setup
We’ll use Spring Boot to create a simple application that uses JPA and Spring Data JPA. Then we’ll look at a few ways to use records while interacting with the database.
3.1. Dependencies
Let’s start by adding the Spring Data JPA dependency to our project:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.0.4</version>
</dependency>
In addition to Spring Data JPA, we’ll also need to configure a database. We can use any SQL database. For example, we can use the in-memory H2 database.
3.2. Entity and Record
Let’s create an entity that we’ll use to interact with the database. We’ll create a Book entity that will be mapped to a book table in the database:
@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
}
Let’s also create a Record that corresponds to the Book entity:
public record BookRecord(Long id, String title, String author, String isbn) {}
Next, we’ll look at a few ways to use the record instead of the entity in our application.
4. Using Records with JPA
The JPA API provides a few ways to interact with the database in which it is possible to use records. Let’s look at a few of them.
4.1. Criteria Builder
Let’s start by looking at how to use records with CriteriaBuilder. We’ll make a query that returns all the books in the database:
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();
}
}
In the above code, we use CriteriaBuilder to create a CriteriaQuery that returns a BookRecord.
Let’s look at some of the steps in the above code:
- We create a CriteriaQuery using the CriteriaBuilder.createQuery() method. We pass the class of the record that we want to return as the parameter
- We then create a Root using the CriteriaQuery.from() method. We pass the entity class as the parameter. This is how we specify the table that we want to query
- Then, we use the CriteriaQuery.select() method to specify a select clause. We use the CriteriaBuilder.construct() method to convert the query results to a record. We pass the class of the record and the fields of the entity that we want to pass to the record constructor as the parameters
- Finally, we use the EntityManager.createQuery() method to create a TypedQuery from CriteriaQuery. We then use the TypedQuery.getResultList() method to get the results of the query
This will create a select query to get all the books in the database. It’ll then convert each result to a BookRecord using the construct() method and return a list of records instead of a list of entities when we call the getResultList() method.
In this way, we can use the entity class to create a query but use records for the rest of the application.
4.2. Typed Query
Similar to the CriteriaBuilder, we can use a typed query to return a record instead of an entity. Let’s add a method in our QueryService to get a single book as records using a typed query:
public BookRecord findBookByTitle(String title) {
TypedQuery<BookRecord> query = entityManager
.createQuery("SELECT new com.baeldung.recordswithjpa.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 allows us to convert the results of a query into any type as long as the type has a constructor that takes the same number of parameters as the results of the query.
In the above code, we use the EntityManager.createQuery() method to create a TypedQuery. We pass the query string and the class of the record as the parameters. Then, we use the TypedQuery.setParameter() method to set the parameters of the query. Finally, we use the TypedQuery.getSingleResult() method to get the result of the query, which will be a BookRecord object.
4.3. Native Query
We can also use native queries to get the results of a query as records. However, a native query doesn’t allow us to convert the results into any type. Instead, we need to use a mapping to convert the results into a record. First, let’s define a mapping in our entity:
@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 {
// ...
}
The mapping will work in the following way:
- The name attribute of the @SqlResultSetMapping annotation specifies the name of the mapping.
- The @ConstructorResult annotation specifies that we want to use the constructor of the record to convert the results.
- The targetClass attribute of the @ConstructorResult annotation specifies the class of the record.
- The @ColumnResult annotation specifies the column name and the type of the column. These column values will be passed to the constructor of the record.
We can then use this mapping in our native query to get the results as records:
public List<BookRecord> findAllBooksUsingMapping() {
Query query = entityManager.createNativeQuery("SELECT * FROM book", "BookRecordMapping");
return query.getResultList();
}
This will create a native query that returns all the books in the database. It’ll convert the results into a BookRecord using the mapping and return a list of records instead of a list of entities when we call the getResultList() method.
5. Using Records with Spring Data JPA
Spring Data JPA provides a few improvements to the JPA API. It enables us to use records with Spring Data JPA repositories in a few ways. Let’s look at how we can use records with Spring Data JPA repositories.
5.1. Automatic Mapping from Entity to Record
Spring Data Repositories allow us to use records as the return type of the methods in the repository. This will automatically map the entity to the record. This is only possible if the record has exactly the same fields as the entity. Let’s look at an example:
public interface BookRepository extends JpaRepository<Book, Long> {
List<BookRecord> findBookByAuthor(String author);
}
Since the BookRecord has the same fields as the Book entity, Spring Data JPA will automatically map the entity to the record and return a list of records instead of a list of entities when we call the findBookByAuthor() method.
5.2. Using Records with @Query
Similar to TypedQuery, we can use records with the @Query annotation in Spring Data JPA repositories. Let’s look at an example:
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);
}
Spring Data JPA will automatically convert the results of the query into a BookRecord and return a single record instead of an entity when we call the findBookById() method.
5.3. Custom Repository Implementation
In case automatic mapping is not an option, we can also define a custom repository implementation that allows us to define our own mapping. Let’s start by creating a CustomBookRecord class that will be used as the return type of the methods in the repository:
public record CustomBookRecord(Long id, String title) {}
Please note that the CustomBookRecord class doesn’t have the same fields as the Book entity. It only has the id and the title fields.
Then, we can create a custom repository implementation that will use the CustomBookRecord class:
public interface CustomBookRepository {
List<CustomBookRecord> findAllBooks();
}
In the implementation of the repository, we can define the methods that will be used to map the results of the queries into the CustomBookRecord class:
@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")));
}
}
In the above code, we use the JdbcTemplate.query() method to execute the query and map the results into a CustomBookRecord using a lambda expression which is an implementation of the RowMapper interface.
6. Using Records as @Embeddables
With the recent updates, Hibernate now supports mapping Java records as embeddable. We can use Java records to represent a group of related properties we want to embed within an entity class:
In this example, the Author is a Java record marked as @Embeddable. It has two fields: firstName and lastName. We can use this record in an entity class to represent an author. To use this Record inside our Entity, we should mark it with @Embedded annotation:
@Entity
@Table(name = "embeadable_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 before the 6.2 version requires some additional work to use Records. The Author field would need additional annotation @EmbeddableInstantiator(AuthorInstallator.class), and we should provide the implementation for the EmbeddableInstantiator interface:
public class AuthorInstallator implements EmbeddableInstantiator {
public boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) {
return object instanceof Author;
}
public boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) {
return object.getClass().equals(Author.class);
}
@Override
public Object instantiate(final ValueAccess valueAccess, final SessionFactoryImplementor sessionFactoryImplementor) {
final String firstName = valueAccess.getValue(0, String.class);
final String secondName = valueAccess.getValue(1, String.class);
return new Author(firstName, secondName);
}
}
Furthermore, Hibernate also supports using structured SQL types to persist to a struct. The Author record can be marked with @Struct annotation that allows mapping the record to a structured SQL type. This can be useful when we want to map the record to a complex type in the database corresponding to a struct.
7. Conclusion
In this article, we’ve looked at how we can use records with JPA and Spring Data JPA. We’ve seen how we can use records with the JPA API using CriteriaBuilder, TypedQuery, and native queries. We’ve also seen how we can use records with Spring Data JPA repositories using automatic mapping, custom queries, and custom repository implementations.
The code examples for this article are available over on GitHub.