How does orphanRemoval work 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, we are going to see how the JPA and Hibernate orphanRemoval mechanism allows us to trigger an entity child remove
operation upon disassociating the child entity reference from the child collection on the parent side.
Domain Model
We are going to use a Post
and a PostComment
entity that forms a one-to-many table relationship:
The @ManyToOne
annotation in the PostComment
entity map the post_id
Foreign Key column that forms the one-to-many table relationship between the parent post
and the post_comment
child tables:
@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; //Getters and setters omitted for brevity }
And the Post
entity is mapped as follows:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id @GeneratedValue private Long id; private String title; @OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private List<PostComment> comments = new ArrayList<>(); //Getters and setters omitted for brevity 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 comments
collection is mapped using the @OneToMany
annotation, and the mappedBy
attribute instructs the JPA provider that the post
property in the PostComment
child entity manages the underlying Foreign Key column.
The cascade
attribute is set to CascadeType.ALL
, meaning that all the JPA and Hibernate entity state transitions (e.g., persist
, merge
, remove
) are passed from the parent Post
entity to the PostComment
child entities.
The orphanRemoval
attribute is going to instruct the JPA provider to trigger a remove
entity state transition when a PostComment
entity is no longer referenced by its parent Post
entity.
Because we have a bidirectional one-to-many association, we need to make sure that both sides of the association are in sync, and for this reason, we created the addComment
and removeComment
methods to synchronize both ends upon adding or removing a new child entity. Check out this article for more details.
JPA and Hibernate CascadeType.PERSIST mechanism
Let’s create a Post
entity with two PostComment
child entities:
Post post = new Post() .setTitle("High-Performance Java Persistence") .addComment( new PostComment() .setReview("Best book on JPA and Hibernate!") ) .addComment( new PostComment() .setReview("A must-read for every Java developer!") ); entityManager.persist(post);
Because the CascadeType.ALL
strategy includes the CascadeType.PERSIST
option, when calling persist
and the post
entity, Hibernate is going to persist both the Post
and the two PostComment
child entities, as illustrated by the generated INSERT statements:
INSERT INTO post ( title, id ) VALUES ( 'High-Performance Java Persistence', 1 ) INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'Best book on JPA and Hibernate!', 2 ) INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'A must-read for every Java developer!', 3 )
JPA and Hibernate orphanRemoval mechanism
If we load the Post
entity along with its two PostComment
child entities and remove the first PostComment
:
Post post = entityManager.createQuery(""" select p from Post p join fetch p.comments c where p.id = :id order by p.id, c.id """, Post.class) .setParameter("id", postId) .getSingleResult(); post.removeComment(post.getComments().get(0));
Hibernate is going to execute the following SQL statements:
SELECT p.id as id1_0_0_, c.id as id1_1_1_, p.title as title2_0_0_, c.post_id as post_id3_1_1_, c.review as review2_1_1_ FROM post p INNER JOIN post_comment c ON p.id = c.post_id WHERE p.id = 1 ORDER BY p.id, c.id DELETE FROM post_comment WHERE id = 2
Because the removeComment
method removes the PostComment
reference from the comments
collection, the orphanRemoval mechanism is going to trigger a remove
on the PostComment
entity, and a DELETE statement is executed.
If we set the orphanRemoval
attribute to the value of false
:
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = false ) private List<PostComment> comments = new ArrayList<>();
And rerun the previous test case, which was calling the removeComment
method, Hibernate executed the following SQL statements:
SELECT p.id as id1_0_0_, c.id as id1_1_1_, p.title as title2_0_0_, c.post_id as post_id3_1_1_, c.review as review2_1_1_ FROM post p INNER JOIN post_comment c ON p.id = c.post_id WHERE p.id = 1 ORDER BY p.id, c.id UPDATE post_comment SET post_id = NULL, review = 'Best book on JPA and Hibernate!' WHERE id = 2
So, instead of a DELETE statement, an UPDATE statement is executed instead, setting the post_id
column to the value of NULL
. This behavior is caused by the following line in the removeComment
method:
comment.setPost(null);
So, if you want to delete the underlying child record upon removing the associated entity from the child collection in the parent entity, then you should set the
orphanRemoval
attribute to the value oftrue
.
JPA and Hibernate orphanRemoval vs. CascadeType.REMOVE
A very common question is how the orphanRemoval mechanism differs from the CascadeType.REMOVE
strategy.
If the orphanRemoval mechanism allows us to trigger a remove
operation on the disassociated child entity, the CascadeType.REMOVE
strategy propagates the remove
operation from the parent to all the child entities.
Because the comments
collection uses CascadeType.ALL
, it means it also inherits the CascadeType.REMOVE
strategy.
Therefore, if we execute the following test case:
Post post = entityManager.createQuery(""" select p from Post p join fetch p.comments where p.id = :id """, Post.class) .setParameter("id", postId) .getSingleResult(); entityManager.remove(post);
Hibernate is going to execute three DELETE statements:
DELETE FROM post_comment WHERE id = 2 DELETE FROM post_comment WHERE id = 3 DELETE FROM post WHERE id = 1
First, the child rows are deleted, because if we deleted the post
row first, a ConstraintViolationExeption
would be triggered since there would be still post_comment
rows associated with the post
record that was wanted to be deleted.
Don’t use CascadeType.REMOVE with @ManyToMany associations
The CascadeType.REMOVE
strategy is useful for the @OneToMany
and @OneToOne
associations only. If you are using a @ManyToMany
association, you should never set the CascadeType.ALL
attribute value, as you’ll also inherit the CascadeType.REMOVE
strategy.
Cascading makes sense from a parent entity to a child. Because a collection annotated with @ManyToMany
associates two parent entities via a join table, we don’t want to propagate the remove from one parent to another. Instead, we want to propagate the remove operation from the parent to the join table child records.
When removing an element from the @ManyToMany
collection, Hibernate generates a DELETE statement for the join table record. So, it works like orphanRemoval, but instead of propagating the remove to the actual entity that is removed from the collection, it triggers the DELETE statement for the child row in the join table.
For more details about this topic, 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
The orphanRemoval strategy simplifies the child entity state management, as we only have to remove the child entity from the child collection, and the associated child record is deleted as well.
Unlike the orphanRemoval strategy, the CascadeType.REMOVE
propagates the remove
operation from the parent to the child entities, as if we manually called remove
on each child entity.
