7

How to cascade DELETE with Spring and Hibernate events

 9 months ago
source link: https://vladmihalcea.com/cascade-delete-hibernate-events/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Last modified: Dec 14, 2023

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 post table root and its unidirectional table relationships

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 Post root entity
  • one PostDetails child entity
  • two PostComment child entities, each one with a UserVote 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.

HPJP_h200.jpg

HPJP_Video_Vertical_h200.jpg

HPSQL_Video_Course_h200.jpg

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.

Transactions and Concurrency Control eBook

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK