Hibernate SoftDelete annotation

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 use the Hibernate SoftDelete annotation to activate soft deleting for JPA entities.

While, as I explained in this article, you can manually implement the soft delete mechanism using the @SQLDelete, @Loader and @Where annotations, it’s definitely much easier to just use the native Hibernate mechanism introduced in Hibernate 6.4 , which you can enable via the @SoftDelete annotation.

Domain Model

To demonstrate how the Hibernate SoftDelete annotation works, we are going to reuse the same entity model that I have previously employed in this article, in which I demonstrated that we can implement a custom soft delete mechanism using the @SQLDelete, @Loader, and @Where annotations.

Soft Delete Domain Model

The Tag entity is mapped like this:

@Entity
@Table(name = "tag")
@SoftDelete
public class Tag {
⠀
    @Id
    @GeneratedValue
    private Long id;
⠀
    @NaturalId
    private String name; 
}

The @SoftDelete annotation was introduced in Hibernate 6.4 and allows us to enable the native soft deleting mechanism.

The Post entity is mapped as follows:

@Entity
@Table(name = "post")
@SoftDelete
public class Post {
⠀
    @Id
    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")
    )
    @SoftDelete
    private List<Tag> tags = new ArrayList<>();
⠀
    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;
    }
}

Notice that both the Post entity and the tags collection use the @SoftDelete Hibernate annotation.

While the former is to soft delete the post table record, the latter is to soft delete the post_tag table rows.

The post_details table is mapped to the PostDetails entity using the @MapsId annotation that allows us to reuse the Primary Key between the parent post and the child post_details table like this:

@Entity
@Table(name = "post_details")
@SoftDelete
public class PostDetails {
⠀
    @Id
    private Long id;
⠀
    @Column(name = "created_on")
    private Date createdOn;
⠀
    @Column(name = "created_by")
    private String createdBy;
⠀
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id")
    @MapsId
    private Post post;
}

The post_comments table is mapped to the PostComment entity as follows:

@Entity
@Table(name = "post_comment")
@SoftDelete
public class PostComment {
⠀
    @Id
    private Long id;
⠀
    @ManyToOne(fetch = FetchType.LAZY)
    @NotFound(action = NotFoundAction.EXCEPTION)
    private Post post;
⠀
    private String review;
}

When we are using the @SoftDelete annotation, you need to also add the @NotFound annotation on the @ManyToOne associations that use the FetchType.LAZY strategy. This is because the presence of the Foreign Key column does not necessarily imply that the parent entity still exists since it could have been soft deleted.

Testing the Hibernate SoftDelete annotation on the Tag entity

Assuming we have created the following Tag entities:

entityManager.persist(
    new Tag().setName("Java")
);

entityManager.persist(
    new Tag().setName("JPA")
);

entityManager.persist(
    new Tag().setName("Hibernate")
);

entityManager.persist(
    new Tag().setName("Misc")
);

When deleting one of the Tag entities:

Tag miscTag = entityManager.unwrap(Session.class)
    .bySimpleNaturalId(Tag.class)
    .getReference("Misc");

entityManager.remove(miscTag);

Hibernate will execute the following UPDATE statement that sets the deleted column to the value of true:

UPDATE tag 
SET 
    deleted = true 
WHERE 
    id = 4 AND 
    deleted = false

After the Tag entity was soft deleted, we can see that the JPQL query won’t be able to find it:

Boolean exists = entityManager.createQuery("""
    select count(t) = 1
    from Tag t
    where t.name = :name
    """, Boolean.class)
.setParameter("name", "Misc")
.getSingleResult();

assertFalse(exists);

Behind the scenes, Hibernate adds the deleted = false predicate in the WHERE clause when referencing entities that are annotated with the @SoftDelete annotation:

SELECT 
    count(t.id)=1 
FROM 
    tag t 
WHERE 
    t.name = 'Misc' AND 
    t.deleted = false

Testing the Hibernate SoftDelete annotation on the PostDetails entity

Considering that we have a Post entity having a one-to-one PostDetails child entity:

Post post = new Post()
    .setId(1L)
    .setTitle("High-Performance Java Persistence");

post.addDetails(
    new PostDetails()
        .setCreatedOn(
            Timestamp.valueOf(
                LocalDateTime.of(2023, 7, 20, 12, 0, 0)
            )
        )
);

entityManager.persist(post);

When deleting PostDetails via the removeDetails method on the parent Post entity:

Post post = entityManager.find(Post.class, 1L);
assertNotNull(post.getDetails());

post.removeDetails();

Hibernate generates the following SQL UPDATE statement that soft deletes the post_details record:

UPDATE 
    post_details 
SET 
    deleted = true 
WHERE 
    id = 1 AND 
    deleted = false

After the PostDetails entity is soft deleted, we won’t be able to fetch it using the find method:

assertNull(entityManager.find(PostDetails.class, 1L));

Testing the Hibernate SoftDelete annotation on the PostComment entity

Let’s consider that we have a Post entity that has two PostComment child entities:

entityManager.persist(
    new Post()
        .setId(1L)
        .setTitle("High-Performance Java Persistence")
        .addComment(
            new PostComment()
                .setId(1L)
                .setReview("Great!")
        )
        .addComment(
            new PostComment()
                .setId(2L)
                .setReview("To read")
        )
);

When deleting one PostComment entity via the removeComment method on the parent Post entity:

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

assertNotNull(entityManager.find(PostComment.class, 2L));

post.removeComment(post.getComments().get(1));

Hibernate generates the following UPDATE statement:

UPDATE 
    post_comment
SET 
    deleted = true 
WHERE 
    id = 2 AND 
    deleted = false

After the PostComment is soft deleted, Hibernate will hide the PostComment entity when trying to fetch it either directly via the find method or indirectly when fetching the comments collection:

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

assertNull(entityManager.find(PostComment.class, 2L));

Testing the Hibernate SoftDelete annotation on the Post entity

If we create a Post entity having all the associations set as in the following example:

entityManager.persist(
    new Post()
        .setId(1L)
        .setTitle("High-Performance Java Persistence")
        .addComment(
            new PostComment()
                .setId(1L)
                .setReview("Great!")
        )
        .addComment(
            new PostComment()
                .setId(2L)
                .setReview("To read")
        )
);

When removing the Post entity:

Post post = entityManager.createQuery("""
    select p
    from Post p
    join fetch p.comments
    join fetch p.details
    where p.id = :id
    """, Post.class)
.setParameter("id", 1L)
.getSingleResult();

entityManager.remove(post);

Hibernate generates the following SQL UPDATE statements:

Query:["update post_tag set deleted=true where post_id=? and deleted=false"], Params:[(1)]
Query:["update post_comment set deleted=true where id=? and deleted=false"], Params:[(1)]
Query:["update post_comment set deleted=true where id=? and deleted=false"], Params:[(2)]
Query:["update post_details set deleted=true where id=? and deleted=false"], Params:[(1)]
Query:["update post set deleted=true where id=? and deleted=false"], Params:[(1)]

Notice that every table record is soft deleted by simply using the @SoftDelete Hibernate annotation.

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

Conclusion

The Hibernate SoftDelete annotation is very easy to use compared to the previous mechanism that we had to implement.

If you want to benefit from this native mechanism, then you should upgrade your Hibernate version to 6.4 or a newer version.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.