The best way to use Java Records with JPA and Hibernate
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
In this article, I’m going to show you how you can use Java Records with JPA and Hibernate.
Added since version 14 as a preview feature, Java Records allow us to create compact DTOs (Data Transfer Objects) or Value Objects.
Domain Model
Let’s assume we have the following Post
entity class in our application:
Notice that the Post
entity uses the Fluent-style API, which allows us to build entity instances like this:
entityManager.persist( new Post() .setId(1L) .setTitle("High-Performance Java Persistence") .setCreatedBy("Vlad Mihalcea") .setCreatedOn( LocalDateTime.of(2016, 11, 2, 12, 0, 0) ) .setUpdatedBy("Vlad Mihalcea") .setUpdatedOn( LocalDateTime.now() ) ); entityManager.persist( new Post() .setId(2L) .setTitle("Hypersistence Optimizer") .setCreatedBy("Vlad Mihalcea") .setCreatedOn( LocalDateTime.of(2020, 3, 19, 12, 0, 0) ) .setUpdatedBy("Vlad Mihalcea") .setUpdatedOn( LocalDateTime.now() ) );
Can Java Records be used as JPA or Hibernate entities?
One very common question is if Java records are going to simplify the way we are building JPA or Hibernate entities. And, the answer is no. They will not.
According to the JPA specification, an entity must follow these requirements:
- the entity class needs to be non-
final
, - the entity class needs to have a no-arg constructor that is either
public
orprotected
, - the entity attributes must be non-
final
.
However, as explained by this article, the Java Record type is defined like this:
- the associated Java class is
final
, - there is only one constructor that takes all attributes,
- the Java record attributes are
final
.
So, according to the JPA specifications, a Java Record cannot be used as an entity.
More, even if Hibernate relaxed these requirements, a Java Record would defeat the purpose of translating entity state transitions into SQL statements.
The Persistence Context managed entities that are meant to be mutable so that, at flush time, the dirty checking mechanism can generate UPDATE statements.
Hibernate already supports read-only entities via the @Immutable
annotation, but the entity classes and the attributes have to be non-final. Otherwise, it would not be possible to fetch lazy associations on demand.
A Java Record is not suitable to be used as a JPA or Hibernate entity.
Using Java Records as DTOs
Let’s assume we have the following PostInfo
and AuditInfo
DTO classes:
With Java Records, we can define the AuditInfo
like this:
public record AuditInfo( LocalDateTime createdOn, String createdBy, LocalDateTime updatedOn, String updatedBy ) {}
and the PostInfo
looks as follows:
public record PostInfo( Long id, String title, AuditInfo auditInfo ) {}
Now, to use the simple class name instead of the fully-qualified one in JPQL queries, we are going to register the AuditInfo
and PostInfo
Java Records using the ClassImportIntegrator
provided by the Hibernate Types project:
properties.put( "hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList( new ClassImportIntegrator( List.of( AuditInfo.class, PostInfo.class ) ) ) );
For more details about the
ClassImportIntegrator
utility, check out this article.
Using Java Records in JPA constructor expression queries
To fetch an AuditInfo
DTO projection for a given Post
, we can use the following JPQL query:
AuditInfo auditInfo = entityManager.createQuery(""" select new AuditInfo ( p.createdOn, p.createdBy, p.updatedOn, p.updatedBy ) from Post p where p.id = :postId """, AuditInfo.class) .setParameter("postId", 1L) .getSingleResult();
Thanks to multiline Java Text Blocks, the JPQL query is very straight-forward.
While you can also fetch the AuditInfo
using as @SqlResultSetMapping
, neither the JPQL constructor Expression nor the @SqlResultSetMapping
allows you to fetch the PostInfo
as you need to pass a properly instantiated AuditInfo
reference in the PostInfo
constructor.
Using Java Records using the Hibernate ResultTransformer
Where JPA falls short, Hibernate comes to the rescue. Thanks to the [ResultTransformer](https://vladmihalcea.com/hibernate-resulttransformer/) Hibernate feature, you can fetch the
PostInfoand
AuditInfo` Java Records together:
List<PostInfo> postInfos = entityManager.createQuery(""" select p.id, p.title, p.createdOn, p.createdBy, p.updatedOn, p.updatedBy from Post p order by p.id """) .unwrap(Query.class) .setResultTransformer( (ListResultTransformer) (tuple, aliases) -> { int i =0; return new PostInfo( ((Number) tuple[i++]).longValue(), (String) tuple[i++], new AuditInfo( (LocalDateTime) tuple[i++], (String) tuple[i++], (LocalDateTime) tuple[i++], (String) tuple[i++] ) ); } ) .getResultList();
Thanks to the ListResultTransformer
utility, that’s also offered by the amazing Hibernate Types project, we can use a Java Lambda function to define the Hibernate ResultTransformer
logic.
For more details about DTO projections with JPA and Hibernate, check out this article.
I'm running an online workshop on the 11th of October about High-Performance SQL.If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
So, while Java Records are not suitable for mapping JPA and Hibernate entities, they are very useful for DTO projections.
Fetching DTOs is a very common requirement for read-only data that needs to be feed to the UI layer, so Java Records can ease the process of defining DTO projections.
