5

Soft delete and JPA version property

 1 year ago
source link: https://vladmihalcea.com/soft-delete-jpa-version/
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: Jul 21, 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 implement a soft delete mechanism when the JPA entity features a version property.

If you are not using a version property for optimistic locking, then you can implement the soft delete strategy, as explained in this article.

Domain Model

Let’s assume we have the following Post, PostDetails, PostComment, and Tag entities that extend the SoftDeletable base class, as illustrated by the following diagram:

Soft Delete With JPA Version Property

Soft delete when the entity has a JPA version property

Now, we want to instruct Hibernate to execute an UPDATE statement that sets the deleted column to the value of true when a Post, PostDetails, PostComments, or Tag entity is deleted.

To accomplish this task, we will need to use the following Hibernate-specific annotations:

  • @SQLDelete – to override the SQL statements that’s executed whenever an entity is deleted so that we run an UPDATE statement instead.
  • @Loader and – to override the default query that loads the entity when using a find method. Using a custom SQL query, we can filter out the records that have the deleted column set to the value of true.
  • @Where – to override the default query plan when loading the entity using an entity query so that we filter out the records that have the deleted column set to the value of true.

Soft deleting the Tag entity

For the Tag entity, we will use these Hibernate annotations as follows:

@Entity(name = "Tag")
@Table(name = "tag")
@SQLDelete(sql = """
UPDATE tag
SET deleted = true
WHERE
id = ? AND
version = ?
""")
@Loader(namedQuery = "findTagById")
@NamedQuery(name = "findTagById", query = """
select t
from Tag t
where
t.id = ?1 and
t.deleted = false
""")
@Where(clause = "deleted = false")
public class Tag extends SoftDeletable {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String name;
@Version
private short version;
}

Notice that the @SqlDelete query uses the version property as well when deleting the entity so that we can prevent lost updates.

If you wonder why the version property is mapped as a Short, and not as Integer or Long, then check out this article about the benefits of using a compact optimistic locking property.

Now, let’s assume we have the following data in the database:

entityManager.persist(new Tag().setName("Java"));
entityManager.persist(new Tag().setName("JPA"));
entityManager.persist(new Tag().setName("Hibernate"));
entityManager.persist(new Tag().setName("Misc"));
Post post = new Post().setTitle("High-Performance Java Persistence");
entityManager.persist(post);
Session session = entityManager.unwrap(Session.class);
post.addTag(session.bySimpleNaturalId(Tag.class).getReference("Java"));
post.addTag(session.bySimpleNaturalId(Tag.class).getReference("Hibernate"));
post.addTag(session.bySimpleNaturalId(Tag.class).getReference("Misc"));

Now, when removing one Tag entity:

Tag miscTag = doInJPA(entityManager -> {
return entityManager
.unwrap(Session.class)
.bySimpleNaturalId(Tag.class)
.getReference("Misc");
});
doInJPA(entityManager -> {
entityManager.remove(miscTag);
});

We can see that Hibernate executes an UPDATE statement that sets the deleted column to the value of true for the tag record that’s just got deleted:

UPDATE
tag
SET
deleted = true
WHERE
id = 4 AND
version = 0

When loading the Post entity, we can see that it now has only 2 Tag entities associated since we deleted one entry:

Post post = entityManager.find(Post.class, 1L);
assertEquals(2, post.getTags().size());

While the tag record with the value of Misc was not physically deleted, the Post entity can no longer see it since the following SQL query is executed to load the associated Tag entities:

SELECT
pt.post_id,
t1_1.id,
t1_1.deleted,
t1_1.name,
t1_1.version
FROM
post_tag pt
JOIN
tag t1_1 ON t.id = pt.tag_id
WHERE
pt.post_id = 1 AND
(t.deleted = FALSE)

If we try to load the Tag entity that was soft deleted using a find call, we can see that no entity is returned if the Misc tag is still present in the database table, as illustrated by the native SQL query that follows the find call:

assertNull(entityManager.find(Tag.class, miscTag.getId()));
assertEquals(
miscTag.getName(),
entityManager.createNativeQuery("""
SELECT
name
FROM
tag
WHERE
id = :id 
""")
.setParameter("id", miscTag.getId())
.getSingleResult()
);

The soft-deleted entities are filtered even when executing JPQL queries, so when loading all Tag entities:

List<Tag> tags = entityManager.createQuery("""
select t
from Tag t
""", Tag.class)
.getResultList();
assertEquals(3, tags.size());

Hibernate executes the following SQL query that filters the tag rows by their deleted column value:

SELECT
t1_0.id,
t1_0.deleted,
t1_0.name,
t1_0.version
FROM
tag t1_0
WHERE
(t1_0.deleted = FALSE)

Soft delete the other entities with a JPA version property

The other entities follow exactly the same soft delete mappings we used for the Tag entity.

For instance, the PostComment entity is mapped as follows:

@Entity(name = "PostComment")
@Table(name = "post_comment")
@SQLDelete(sql = """
UPDATE post_comment
SET deleted = true
WHERE
id = ? AND
version = ?
""")
@Loader(namedQuery = "findPostCommentById")
@NamedQuery(name = "findPostCommentById", query = """
select pc
from PostComment pc
where
pc.id = ?1 and
pc.deleted = false
""")
@Where(clause = "deleted = false")
public class PostComment extends SoftDeletable {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
private String review;
@Version
private short version; 
}

The PostDetails is mapped like this:

@Entity(name = "Post")
@Table(name = "post")
@SQLDelete(sql = """
UPDATE post
SET deleted = true
WHERE
id = ? AND
version = ?
""")
@Loader(namedQuery = "findPostById")
@NamedQuery(name = "findPostById", query = """
select p
from Post p
where
p.id = ?1 and
p.deleted = false
""")
@Where(clause = "deleted = false")
public class Post extends SoftDeletable {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
@OneToOne(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private PostDetails details;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "post_tag",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags = new ArrayList<>();
@Version
private short version;
}

And the Post entity is mapped as follows:

@Entity(name = "Post")
@Table(name = "post")
@SQLDelete(sql = """
UPDATE post
SET deleted = true
WHERE
id = ? AND
version = ?
""")
@Loader(namedQuery = "findPostById")
@NamedQuery(name = "findPostById", query = """
select p
from Post p
where
p.id = ?1 and
p.deleted = false
""")
@Where(clause = "deleted = false")
public class Post extends SoftDeletable {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
@OneToOne(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private PostDetails details;
@ManyToMany
@JoinTable(
name = "post_tag",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags = new ArrayList<>();
@Version
private short version;
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;
}
public Post addDetails(PostDetails details) {
this.details = details;
details.setPost(this);
return this;
}
public Post removeDetails() {
this.details.setPost(null);
this.details = null;
return this;
}
public Post addTag(Tag tag) {
tags.add(tag);
return this;
}
}

Assuming we have persisted the following Post entity:

entityManager.persist(
new Post()
.setTitle("High-Performance Java Persistence")
.addDetails(new PostDetails().setCreatedBy("Vlad Mihalcea"))
.addComment(new PostComment().setReview("Excellent!"))
.addComment(new PostComment().setReview("Great!"))
);

When fetching a Post entity along with associated PostDetails child entity and removing the PostDetails:

Post post = entityManager.createQuery("""
select p
from Post p
join fetch p.details
where p.title = :title 
""", Post.class)
.setParameter("title", "High-Performance Java Persistence")
.getSingleResult();
assertNotNull(post.getDetails());
post.removeDetails();

We can see that Hibernate executed an UPDATE statement to set the deleted column of the associated post_details record to the value of true:

UPDATE
post_details
SET
deleted = true
WHERE
id = 1 AND
version = 0

The same applies to the PostComment entity, so when loading the Post entity along with its associated PsotComment child entities and removing one of the comments:

Post post = entityManager.createQuery("""
select p
from Post p
left join fetch p.comments
where p.title = :title 
""", Post.class)
.setParameter("title", "High-Performance Java Persistence")
.getSingleResult();
post.removeComment(post.getComments().get(0));

Hibernate will soft delete the associated post_comment record by executing an UPDATE statement instead of a DELETE:

UPDATE
post_comment
SET
deleted = true
WHERE
id = 1 AND
version = 0

That’s it!

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

Hibernate is a very flexible JPA provider implementation, allowing you to customize its default behavior based on your application-specific needs.

If your business requirements include the implementation of a soft delete mechanism, then you can achieve this goal with just a few mappings, no matter if the entity uses the JPA version property or not.

Transactions and Concurrency Control eBook

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK