How to use Java Records with Spring Data JPA
source link: https://vladmihalcea.com/records-spring-data-jpa/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
How to use Java Records with Spring Data JPA
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, we are going to see how we can use Java Records with Spring Data JPA Repositories.
As I already explained, Java Records cannot be used as JPA entities since the Records are immutable, and JPA requires the entity class to have a default constructor and be modifiable, as that’s how the entity properties are populated when the entity is being fetched from the database.
For this reason, this article will show you how to combine Java Records and JPA entities so that you get the best out of both.
Domain Mode
Let’s assume we have the following Post
and PostComment
entities:
The Post
is the parent entity in this one-to-many table relationship and is mapped like this:
@Entity (name = "Post" ) @Table (name = "post" ) public class Post { @Id private Long id; private String title; @OneToMany ( mappedBy = "post" , cascade = CascadeType.ALL, orphanRemoval = true ) private List<PostComment> comments = new ArrayList<>(); public Long getId() { return id; } public Post setId(Long id) { this .id = id; return this ; } public String getTitle() { return title; } public Post setTitle(String title) { this .title = title; return this ; } public List<PostComment> getComments() { return comments; } public Post addComment(PostComment comment) { comments.add(comment); comment.setPost( this ); return this ; } public PostRecord toRecord() { return new PostRecord( id, title, comments.stream().map(comment -> new PostCommentRecord( comment.getId(), comment.getReview() ) ).toList() ); } } |
The PostComment
is the client entity in this relationship and is mapped as follows:
@Entity (name = "PostComment" ) @Table (name = "post_comment" ) public class PostComment { @Id @GeneratedValue private Long id; @ManyToOne (fetch = FetchType.LAZY) private Post post; private String review; public Long getId() { return id; } public PostComment setId(Long id) { this .id = id; return this ; } public Post getPost() { return post; } public PostComment setPost(Post post) { this .post = post; return this ; } public String getReview() { return review; } public PostComment setReview(String review) { this .review = review; return this ; } } |
However, since we don’t want to expose the JPA entities outside of the Service and Data Access Layer, we have created the following PostRecord
and PostCommentRecord
objects that we are going to share with the Web Layer:
The PostRecord
is created like this:
public record PostRecord( Long id, String title, List<PostCommentRecord> comments ) { public Post toPost() { Post post = new Post() .setId(id) .setTitle(title); comments.forEach( comment -> post.addComment(comment.toPostComment()) ); return post; } } |
An the PostCommentRecord
is created as follows:
public record PostCommentRecord( Long id, String review ) { public PostComment toPostComment() { return new PostComment() .setId(id) .setReview(review); } } |
As you can see in the JPA entity mappings and the Record classes, we have created several methods that allow us to create the Record from the JPA entity and vice versa. These methods will be used when exporting the Records from the service layer and importing the Records so that we can propagate the changes to the database via their associated JPA entities.
The Service and Data Access Layer
The Data Access Layer is very simple and consists of the following PostRepository
:
@Repository public interface PostRepository extends BaseJpaRepository<Post, Long> { @Query ( "" " select p from Post p join fetch p.comments where p.id = :postId "" ") Optional<Post> findWithCommentsById( @Param ( "postId" ) Long postId ); } |
The
BaseJpaRepository
is a replacement for the default Spring DataJpaRepository
that removes dangerous methods, such asfindAll
orsave
, providing much better alternatives.For more details about the
BaseJpaRepository
from Hypersistence Utils, check out this article.
The findWithCommentsById
method allows us to fetch a Post
entity along with all its associated PostComment
child entities in a single SQL query.
The Service Layer provides the following ForumService
Spring bean that the Web Layer will interact with:
@Service @Transactional (readOnly = true ) public class ForumService { @Autowired private PostRepository postRepository; public PostRecord findPostRecordById(Long postId) { return postRepository .findWithCommentsById(postId) .map(Post::toRecord) .orElse( null ); } @Transactional public Post insertPostRecord(PostRecord postRecord) { return postRepository .persist(postRecord.toPost()); } @Transactional public Post mergePostRecord(PostRecord postRecord) { return postRepository .merge(postRecord.toPost()); } } |
The findPostRecordById
method fetches the Post
entity and transforms it to a PostRecord
using the toRecord
method.
The insertPostRecord
method takes a PostRecord
object and persists the associated Post
and PostComment
child entities.
The mergePostRecord
method takes a PostRecord
object and merges the associated Post
and PostComment
child entities.
Testing Time
To test how the Java Records can be fetched, persisted, and merged using Spring Data JPA, we are going to call the aforementioned ForumService
methods and see what statements are executed by Hibernate behind the scenes.
If we create a PostRecord
object that has 5 associated PostCommentRecord
child objects and pass the PostRecord
to the insertPostRecord
method:
forumService.insertPostRecord( new PostRecord( 1L, "High-Performance Java Persistence" , LongStream.rangeClosed( 1 , 5 ) .mapToObj(i -> new PostCommentRecord( null , String.format( "Good review nr. %d" , i) ) ) .toList() ) ); |
We can see that Spring Data JPA generates the following SQL INSERT statements:
Query:[ " insert into post (title,id) values (?,?) " ], Params:[ (High-Performance Java Persistence, 1) ] Query:[ " insert into post_comment (post_id,review,id) values (?,?,?) " ], Params:[ (1, Good review nr. 1, 1), (1, Good review nr. 2, 2), (1, Good review nr. 3, 3), (1, Good review nr. 4, 4), (1, Good review nr. 5, 5) ] |
Spring Data JPA can take advantage of the automatic JDBC batch insert feature provided by Hibernate if we configure the following settings:
properties.put( AvailableSettings.STATEMENT_BATCH_SIZE, 50 ); properties.put( AvailableSettings.ORDER_INSERTS, Boolean.TRUE ); |
For more details about the JDBC batch update mechanism, check out this article.
When fetching the PostRecord
object using the findPostRecordById
method:
PostRecord postRecord = forumService.findPostRecordById(1L); |
Hibernate executes the following SQL SELECT query:
Query:[ " select p1_0.id, c1_0.post_id, c1_0.id, c1_0.review, p1_0.title from post p1_0 join post_comment c1_0 on p1_0.id=c1_0.post_id where p1_0.id=? " ], Params:[(1)] |
Now, we could send the PostRecord
as a JSON object to the front-end layer as it’s very easy to convert Java Records to JSON using Jackson:
For instance, if we log the JSON representation of the PostRecord
we’ve just fetched:
LOGGER.info( "PostRecord to JSON: {}" , JacksonUtil.toString(postRecord)); |
We are going to see the following JSON object printed to the log:
{ "id" : 1, "title" : "High-Performance Java Persistence" , "comments" : [ { "id" : 1, "review" : "Good review nr. 1" }, { "id" : 2, "review" : "Good review nr. 2" }, { "id" : 3, "review" : "Good review nr. 3" }, { "id" : 4, "review" : "Good review nr. 4" }, { "id" : 5, "review" : "Good review nr. 5" } ] } |
If the front-end modifies the Post
and PostComment
objects and sends is the following JSON object that we pass to the mergePostRecord
method:
String upatedPostRecordJSONSTring = "" " { "id" : 1 , "title" : "High-Performance Java Persistence, 2nd edition" , "comments" : [ { "id" : 1 , "review" : "Best book on JPA and Hibernate!" }, { "id" : 2 , "review" : "A must-read for every Java developer!" } ] } "" "; forumService.mergePostRecord( JacksonUtil.fromString( upatedPostRecordJSONSTring, PostRecord. class ) ); |
Then we can see that Spring Data JPA will execute the following SQL statements:
Query:[ " select p1_0.id, p1_0.title, c1_0.post_id, c1_0.id, c1_0.review from post p1_0 left join post_comment c1_0 on p1_0.id=c1_0.post_id where p1_0.id=? " ], Params:[(1)] Query:[ " update post set title=? where id=? " ], Params:[(High-Performance Java Persistence, 2nd edition, 1)] Query:[ " update post_comment set post_id=?,review=? where id=? " ], Params:[ (1, Best book on JPA and Hibernate!, 1), (1, A must- read for every Java developer!, 2) ] Query:[ " delete from post_comment where id=? " ], Params:[ (3), (4), (5) ] |
The first SQL SELECT query is executed by merge
in order to fetch the latest state of the Post
and PostComment
entities so that the Hibernate dirty checking mechanism can determine what has changed.
For more details about how the
persist
andmerge
JPA operations work, check out this article.
The UPDATE statements that follow are for the post
and post_comment
table records that have changed, and the DELETE statements are for the records that the client has removed.
I'm running a High-Performance Spring Data JPA Online Workshop on the 12th of March.If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
Java Records can be very useful when using Spring Data JPA because they allow us to isolate the JPA entities to the Service and Data Access Layer.
Transforming the JPA entities to Java Records and from Java Records to JPA entities is rather straightforward, and this way, we don’t have to worry about N+1 query issues or LazyInitializationException
problems as Java Records have all the data already pre-fetched.
For more details about Java Records, check out this article as well.
4 Comments on “How to use Java Records with Spring Data JPA”
-
John
February 24, 2024For the insert use case, how would we know what the ID value should be? In your example you put 1L but can we expect the caller to know what the next ID should be?
-
If you take a look at the
PostComment
, you’ll see that theid
isnull
, as it’s auto-generated.I could have done the same thing for
Post
, but for the sake of readability, I deliberately made thePost
use an assigned generator so that you can see the same id when I insert, select and update it.-
John
February 24, 2024Sorry I missed that, thank you!
-
You’re welcome
-
-
Leave a Reply Cancel reply
Your email address will not be published. Required fields are marked *
Comment *
Before posting the comment, please take the time to read the FAQ page
Name *
Email *
Website
Notify me of follow-up comments by email.
This site uses Akismet to reduce spam. Learn how your comment data is processed.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK