How to cascade DELETE with Spring and Hibernate events
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 cascade the DELETE operation for unidirectional associations with Spring Data JPA and Hibernate events.
Using Hibernate events to achieve this goal is an alternative to the bulk DELETE statement strategy, as it allows us to cascade the delete operation from a parent entity to its children when we cannot use the CascadeType
mechanism.
Domain Model
We are going to use a table relationship model that is similar to the one we used in the article about cascading DELETE using bulk DELETE statements:
The difference between the two models is that this time, our tables use a version
column for optimistic locking, and for this reason, we created the VersionedEntity
base class that all our versioned JPA entities are going to extend:
@MappedSuperclass public class VersionedEntity { @Version private Short version; public Short getVersion() { return version; } public void setVersion(Short version) { this.version = version; } }
If you wonder why the version is mapped to a
Short
, then check out this article for a detailed explanation.
The Post
entity is the root entity of our hierarchy and looks like this:
@Entity @Table(name = "posts") public class Post extends VersionedEntity { @Id private Long id; private String title; //Getters and setters omitted for brevity }
Because all child entities reference the parent Post
entity using unidirectional @ManyToOne
or @OneToOne
associations, there is no bidirectional @OneToMany
or @OneToOne
association that we could use to cascade the DELETE
operation from the parent Post
to the PostComment
, PostDetails
, or PostTag
child entities.
The PostComment
entity maps the post_id
Foreign Key column via a unidirectional @ManyToOne
association:
@Entity @Table(name = "post_comments") public class PostComment extends VersionedEntity { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) private Post post; private String review; //Getters and setters omitted for brevity }
The PostDetails
entity maps the id
Foreign Key column using a unidirectional @OneToOne
association along with the @MapsId
annotation:
@Entity @Table(name = "post_details") public class PostDetails extends VersionedEntity { @Id private Long id; @OneToOne(fetch = FetchType.LAZY) @MapsId @JoinColumn(name = "id") private Post post; @Column(name = "created_on") private LocalDateTime createdOn; @Column(name = "created_by") private String createdBy; //Getters and setters omitted for brevity }
The PostTag
entity maps the post_tag
many-to-many table, and the post_id
Foreign Key column is mapped using a unidirectional @ManyToOne
association:
@Entity @Table(name = "post_tags") public class PostTag extends VersionedEntity { @EmbeddedId private PostTagId id; @ManyToOne(fetch = FetchType.LAZY) @MapsId("postId") private Post post; @ManyToOne(fetch = FetchType.LAZY) @MapsId("tagId") private Tag tag; @Column(name = "created_on") private Date createdOn = new Date(); //Getters and setters omitted for brevity }
To make the Post
hierarchy more complex, we will not just define direct child associations to the root entity. For this reason, we also have a UserVote
entity that is a child of the PostComment
entity and a grandchild association of the Post
root entity:
@Entity @Table(name = "user_votes") public class UserVote extends VersionedEntity { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) private User user; @ManyToOne(fetch = FetchType.LAZY) private PostComment comment; private int score; //Getters and setters omitted for brevity }
Creating the Post entity hierarchy
We will create a Post
entity hierarchy that will contain the following:
- one
Post
root entity - one
PostDetails
child entity - two
PostComment
child entities, each one with aUserVote
entity - three
PostTag
child entities
Post post = new Post() .setId(1L) .setTitle("High-Performance Java Persistence"); postRepository.persist(post); postDetailsRepository.persist( new PostDetails() .setCreatedBy("Vlad Mihalcea") .setPost(post) ); PostComment comment1 = new PostComment() .setReview("Best book on JPA and Hibernate!") .setPost(post); PostComment comment2 = new PostComment() .setReview("A must-read for every Java developer!") .setPost(post); postCommentRepository.persist(comment1); postCommentRepository.persist(comment2); User alice = new User() .setId(1L) .setName("Alice"); User bob = new User() .setId(2L) .setName("Bob"); userRepository.persist(alice); userRepository.persist(bob); userVoteRepository.persist( new UserVote() .setUser(alice) .setComment(comment1) .setScore(Math.random() > 0.5 ? 1 : -1) ); userVoteRepository.persist( new UserVote() .setUser(bob) .setComment(comment2) .setScore(Math.random() > 0.5 ? 1 : -1) ); Tag jdbc = new Tag().setName("JDBC"); Tag hibernate = new Tag().setName("Hibernate"); Tag jOOQ = new Tag().setName("jOOQ"); tagRepository.persist(jdbc); tagRepository.persist(hibernate); tagRepository.persist(jOOQ); postTagRepository.persist(new PostTag(post, jdbc)); postTagRepository.persist(new PostTag(post, hibernate)); postTagRepository.persist(new PostTag(post, jOOQ));
How to cascade DELETE with Spring Data JPA and Hibernate events
Now, if we try to remove the Post
entity we have just created using the default deleteById
method of the PostRepository
:
postRepository.deleteById(1L);
We are going to get the following ConstraintViolationException
:
Caused by: org.postgresql.util.PSQLException: ERROR: update or delete on table "post" violates foreign key constraint "fk_post_comment_post_id" on table "post_comment" Detail: Key (id)=(1) is still referenced from table "post_comment".
The ConstraintViolationException
was thrown because the post
table record is referenced by the child records in the post_details
, post_comment
, and post_tag
tables.
Hence, we need to make sure that we remove all the child entries prior to removing a given Post
entity.
To accomplish this goal, we will add a CascadeDeleteEventListener
, which will intercept the delete
operation of the Post
entity like this:
public class CascadeDeleteEventListener implements DeleteEventListener { public static final CascadeDeleteEventListener INSTANCE = new CascadeDeleteEventListener(); @Override public void onDelete( DeleteEvent event) throws HibernateException { final Object entity = event.getObject(); Session session = event.getSession(); if (entity instanceof Post post) { session.remove( session.find(PostDetails.class, post.getId()) ); session.createQuery(""" select uv from UserVote uv where uv.comment.id in ( select id from PostComment where post.id = :postId ) """, UserVote.class) .setParameter("postId", post.getId()) .getResultList() .forEach(session::remove); session.createQuery(""" select pc from PostComment pc where pc.post.id = :postId """, PostComment.class) .setParameter("postId", post.getId()) .getResultList() .forEach(session::remove); session.createQuery(""" select pt from PostTag pt where pt.post.id = :postId """, PostTag.class) .setParameter("postId", post.getId()) .getResultList() .forEach(session::remove); } } @Override public void onDelete( DeleteEvent event, DeleteContext transientEntities) throws HibernateException { onDelete(event); } }
The reason why we are not using a bulk DELETE statement to remove the associated child table records is that we want to take advantage of the optimistic locking mechanism.
To register the CascadeDeleteEventListener
, we can use the following CascadeDeleteEventListenerIntegrator
:
public class CascadeDeleteEventListenerIntegrator implements Integrator { public static final CascadeDeleteEventListenerIntegrator INSTANCE = new CascadeDeleteEventListenerIntegrator(); @Override public void integrate( Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) { final EventListenerRegistry eventListenerRegistry = sessionFactory .getServiceRegistry() .getService(EventListenerRegistry.class); eventListenerRegistry.prependListeners( EventType.DELETE, CascadeDeleteEventListener.INSTANCE ); } @Override public void disintegrate( SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {} }
To provide the CascadeDeleteEventListenerIntegrator
to Hibernate, we can use the following setting:
properties.put( EntityManagerFactoryBuilderImpl.INTEGRATOR_PROVIDER, (IntegratorProvider) () -> List.of( CascadeDeleteEventListenerIntegrator.INSTANCE ) );
With all these changes in place, when deleting the Post
entity:
postRepository.deleteById(1L);
Spring Data JPA and Hibernate will execute the following SQL statements:
SELECT p.id, p.title, p.version FROM posts p WHERE p.id = 1 SELECT pd.id, pd.created_by, pd.created_on, pd.version FROM post_details pd WHERE pd.id = 1 SELECT uv.id, uv.comment_id, uv.score, uv.user_id, uv.version FROM user_votes uv WHERE uv.comment_id IN( SELECT pc.id FROM post_comments pc WHERE pc.post_id = 1 ) SELECT p1_0.id, p1_0.post_id, p1_0.review, p1_0.version FROM post_comments p1_0 WHERE p1_0.post_id = 1 SELECT p1_0.post_id, p1_0.tag_id, p1_0.created_on, p1_0.version FROM post_tags p1_0 WHERE p1_0.post_id = 1 Query:[" DELETE FROM post_details WHERE id = ? AND version = ? "], Params:[(1, 0)] Query:[" DELETE FROM user_votes WHERE id = ? AND version = ? "], Params:[(1, 0), (2, 0)] Query:[" DELETE FROM post_comments WHERE id = ? AND version = ? "], Params:[(1, 0), (2, 0)] Query:[" DELETE FROM post_tags WHERE post_id = ? AND tag_id = ? AND version = ? "], Params:[(1, 1, 0), (1, 2, 0), (1, 3, 0)] Query:[" DELETE FROM posts WHERE id = ? AND version = ? "], Params:[(1, 0)]
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
While bidirectional associations provide a very simple way to cascade the DELETE operation from the parent entity to child associations, there are other ways you can achieve this goal.
One way is to use bulk DELETE statements, which provides a very efficient way to remove the associated table records.
Another way we can cascade the delete operation is to intercept the Hibernate DeleteEvent
and execute the cleanup logic automatically when deleting a root entity.
