The best way to use JPA bidirectional sync methods
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 what are the best way to use JPA bidirectional sync methods for one-to-many, one-to-one, and many-to-many associations.
For an introduction to why you need sync methods for your bidirectional JPA associations, check out this article first.
Domain Model
For this article, we are going to use the following entities that are linked using bidirectional associations:
- the
Post
has a bidirectional one-to-many association with thePostComment
child entity - the
Post
has a bidirectional one-to-one association with thePostDetails
child entity - the
Post
has a bidirectional many-to-many association with theTag
entity
JPA bidirectional sync methods
In order for the bidirectional associations to work properly, we have defined several sync methods.
In the Post
entity, the addComment
and removeComment
methods provide the synchronization of the bidirectional association:
public Post addComment(PostComment comment) { comments.add(comment); comment.setPost(this); return this; } public Post removeComment(PostComment comment) { comments.remove(comment); comment.setPost(null); return this; }
The setDetails
method allows the Post
and the PostDetails
entities to be synchronized whenever the child entity is added or removed:
public Post setDetails(PostDetails details) { if (details == null) { if (this.details != null) { this.details.setPost(null); } } else { details.setPost(this); } this.details = details; return this; }
And for the many-to-many association, we have the addTag
and removeTag
sync method:
public Post addTag(Tag tag) { tags.add(tag); tag.getPosts().add(this); return this; } public void removeTag(Tag tag) { tags.remove(tag); tag.getPosts().remove(this); }
Now, let’s see when we should use these methods.
Cascading persist from a parent entity
If you have a root entity, it’s very convenient to cascade the persist
entity state transition from the root entity, Post
in our case, to the other entities, like this:
postRepository.persist( new Post() .setId(1L) .setTitle("High-Performance Java Persistence") .setDetails(new PostDetails().setCreatedBy("Vlad Mihalcea")) .addComment( new PostComment() .setReview("Best book on JPA and Hibernate!") ) .addComment( new PostComment() .setReview("A must-read for every Java developer!") ) .addTag(new Tag().setName("JDBC")) .addTag(new Tag().setName("Hibernate")) .addTag(new Tag().setName("jOOQ")) );
By using the bidirectional add
sync methods, we can ensure that the persist
entity state transition is going to be propagated properly. Without synchronizing both sides of the JPA association, it’s not guaranteed that the entity state will be properly synchronized with the database.
Persisting a child entity without fetching the parent entity
If you need to persist
a PostComment
entity but you don’t need to fetch the Post
entity, then you can do it like this:
postCommentRepository.persist( new PostComment() .setPost(postRepository.getReferenceById(1L)) .setReview( "Very informative. Learned a lot, applied every day." ) );
When executing the code snippet above, Hibernate will generate the following SQL INSERT statement:
INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'Very informative. Learned a lot, applied every day.', 3 )
By using getRefernceById
, you can fetch a parent entity Proxy that will set the Foreign Key column value without triggering an unnecessary entity fetch.
If you are interested in the difference between
findById
andgetRefernceById
, check out this article for more details.
Persisting a child entity when the parent entity is already fetched
If you already fetched the parent Post
entity and want to persist
a new PostComment
, then you need to use the addComment
sync method because both the parent and the child will be managed by the current Persistence Context:
Post post = postRepository.findByIdWithDetailsAndComments(1L); post.addComment( new PostComment() .setReview( "Very informative. Learned a lot, applied every day." ) );
When executing the code snippet above, we can see that the INSERT follows the SELECT that we already executed previously.
SELECT p.id, pc.post_id, pc.id, pc.review, pd.post_id, pd.created_by, pd.created_on, p.title FROM post p JOIN post_details pd ON p.id = pd.post_id JOIN post_comment pc ON p.id = pc.post_id WHERE p.id = 1 INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'Very informative. Learned a lot, applied every day.', 3 )
However, if you didn’t fetch the
Post
previously, then we don’t need to fetch it just to add or remove a single child entity.The sync methods are useful when the entity propagating the state transitions is already fetched.
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
When you have bidirectional associations, it’s important to create the sync methods that allow you to set both sides of the bidirectional association.
However, just because you created these methods, it doesn’t mean you need to call them every time you want to persist
or remove
a child entity. You only have to use them if you have already fetched the parent entity. Otherwise, you can simply add or remove the child entity without executing any extra SELECT
statement.
