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:

JPA and Hibernate orphanRemoval entity association

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_commentchild 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 of true.

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 20-21 and 23-24 of November about High-Performance Java Persistence.

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.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.