How to cascade DELETE unidirectional associations with Spring Data JPA
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 to cascade DELETE the unidirectional associations with Spring Data JPA when we cannot rely on the CascadeType
mechanism that propagates state transitions from parent to child entities.
Domain Model
Let’s consider we have the following entities in our system:
The Post
entity is the root of this entity hierarchy, and the child entities use only unidirectional @ManyToOne
or @OneToOne
associations that map the underlying Foreign Key of a one-to-many or one-to-one table relationship.
The root Post
entity looks as follows:
@Entity @Table(name = "post") public class Post { @Id private Long id; private String title; //Getters and setters omitted for brevity }
Notice that there’s no bidirectional @OneToMany
or @OneToOne
association that would allow us 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 using a unidirectional @ManyToOne
association:
@Entity @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 }
The PostDetails
entity maps the id
Foreign Key column using a unidirectional @OneToOne
association:
@Entity @Table(name = "post_details") public class PostDetails { @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_id
Foreign Key column using a unidirectional @ManyToOne
association:
@Entity @Table(name = "post_tag") public class PostTag { @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 }
And the Post
entity doesn’t only have direct child associations since the UserVote
entity is a child of the PostComment
. Hence, it’s a grandchild association of the Post
root entity:
@Entity @Table(name = "user_vote") public class UserVote { @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 a Post entity hierarchy
Let’s assume we have created a Post
entity hierarchy that contains:
- 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 unidirectional associations with Spring Data JPA
Now, we would like to have a way to remove a given Post
entity, and if we use the default deleteById
method of the PostRepository
on the Post
entity we have previously created:
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
is thrown because the post
table record is still referenced by its child entities in the post_details
, post_comment
, and post_tag
tables.
Therefore, we need to make sure we remove all child entries prior to removing a given Post
entity.
To accomplish this goal, we will change the PostRepository
so that it now extends the CustomPostRepository
interface:
@Repository public interface PostRepository extends BaseJpaRepository<Post, Long>, CustomPostRepository<Long> { }
The CustomPostRepository
defines the deleteById
method that we plan on overriding in the custom JPA repository so that we can cascade DELETE operation from the Post
entity to all the unidirectional associations:
public interface CustomPostRepository<ID> { void deleteById(ID postId); }
For an introduction to custom Spring Data JPA Repositories, check out this article.
The CustomPostRepository
interface is implemented by the CustomPostRepositoryImpl
class, which is going to define the cascading delete logic for all the dependencies of the Post
entity:
public class CustomPostRepositoryImpl implements CustomPostRepository<Long> { private final PostDetailsRepository postDetailsRepository; private final UserVoteRepository userVoteRepository; private final PostCommentRepository postCommentRepository; private final PostTagRepository postTagRepository; private final EntityManager entityManager; public CustomPostRepositoryImpl( PostDetailsRepository postDetailsRepository, UserVoteRepository userVoteRepository, PostCommentRepository postCommentRepository, PostTagRepository postTagRepository, EntityManager entityManager) { this.postDetailsRepository = postDetailsRepository; this.userVoteRepository = userVoteRepository; this.postCommentRepository = postCommentRepository; this.postTagRepository = postTagRepository; this.entityManager = entityManager; } @Override public void deleteById(Long postId) { postDetailsRepository.deleteByPostId(postId); userVoteRepository.deleteAllByPostId(postId); postCommentRepository.deleteAllByPostId(postId); postTagRepository.deleteAllByPostId(postId); entityManager.createQuery(""" delete from Post where id = :postId """) .setParameter("postId", postId) .executeUpdate(); } }
The deleteById
method is implemented in such a way that we can clean up all associated child table records that point to the Post
entity, either directly via a Foreign Key or indirectly via a chain of Foreign Key references, as is the case with the user_vote
table records.
Therefore, we will first delete the associated PostDetails
child entities by calling the deleteByPostId
method from the PostDetailsRepository
using a bulk DELETE statement:
@Repository public interface PostDetailsRepository extends BaseJpaRepository<PostDetails, Long> { @Query(""" delete from PostDetails where post.id = :postId """) @Modifying void deleteByPostId(@Param("postId") Long postId); }
In the second step, we delete the UserVotes
by calling the deleteAllByPostId
method from the UserVoteRepository
via a bulk DELETE statement:
@Repository public interface UserVoteRepository extends BaseJpaRepository<UserVote, Long> { @Query(""" delete from UserVote where comment.id in ( select id from PostComment where post.id = :postId ) """) @Modifying void deleteAllByPostId(@Param("postId") Long postId); }
Next, we delete the PostComment
child entities by calling the deleteAllByPostId
method from the PostCommentRepository
using a single bulk DELETE statement:
@Repository public interface PostCommentRepository extends BaseJpaRepository<PostComment, Long> { @Query(""" delete from PostComment where post.id = :postId """) @Modifying void deleteAllByPostId(@Param("postId") Long postId); }
Afterward, we delete the PostTag
child entities by calling the deleteAllByPostId
method from the PostTagRepository
using a bulk DELETE statement:
@Repository public interface PostTagRepository extends BaseJpaRepository<PostTag, PostTagId> { @Query(""" delete from PostTag where post.id = :postId """) @Modifying void deleteAllByPostId(@Param("postId") Long postId); }
In the end, after deleting all the child unidirectional associations, we can proceed to delete the root Post
entity:
entityManager.createQuery(""" delete from Post where id = :postId """) .setParameter("postId", postId) .executeUpdate();
Spring Data JPA and Hibernate are going to execute the following SQL statements:
DELETE FROM post_details WHERE id = 1 DELETE FROM user_vote WHERE comment_id IN ( SELECT pd.id FROM post_comment pd WHERE pd.post_id = 1 ) DELETE FROM post_comment WHERE post_id = 1 DELETE FROM post_tag WHERE post_id = 1 DELETE FROM post WHERE id = 1
Awesome, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
While you can use bidirectional associations to cascade the DELETE operation from the parent entity to child entities, this is not the only way you can accomplish this task.
By extending a custom Spring Data JPA Repository method, we can cascade DELETE operational to every direct or indirect unidirectional association.
