How to cascade DELETE with Spring and Hibernate events
Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?
What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?
Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.
So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!
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;
}
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;
}
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;
}
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();
}
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;
}
Creating the Post entity hierarchy
We will create a Post entity hierarchy that will contain the following:
- one
Postroot entity - one
PostDetailschild entity - two
PostCommentchild entities, each one with aUserVoteentity - three
PostTagchild 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.






