How to implement a soft delete with Hibernate

By Thorben Janssen

Mapping, Query

In some applications, you don’t want to, or you are not allowed to permanently remove a record from the database. But you still need to remove or hide records which are no longer active. One example could be a user account which you want to keep because it is linked to other business objects which are still used.

You have 2 basic options to keep this information in your system. You can either keep an audit log that documents all changes or perform a soft delete that hides the removed records. I will explain the audit log option in another blog post. Today I want to show you how to implement a soft delete with Hibernate. But before I do that, let me quickly explain what a soft delete is.

What is a soft delete?

A soft delete performs an update to mark a record as deleted instead of removing it from the database table. Common ways to model a soft delete are:

  • a boolean that indicates if the record is active or deleted,
  • an Enumerated which models the state of the record,
  • a timestamp that stores the date and time when the soft delete was performed.

How to implement a soft delete with Hibernate

It’s not that difficult to implement a soft delete with Hibernate. You just have to:

  1. tell Hibernate to perform a SQL UPDATE instead of a DELETE operation and
  2. exclude all “deleted” records from your query results.

I’ll show you how you can easily do that in this post. All examples will use the following Account entity which uses the AccountState enum to indicate if an account is INACTIVE, ACTIVE or DELETED.

Update the record instead of deleting it

To implement a soft delete, you need to override Hibernate’s default remove operation. You can do that with an @SQLDelete annotation. This annotation allows you to define a custom, native SQL query that Hibernate will execute when you delete the entity. You can see an example of it in the following code snippet.

The @SQLDelete annotation in the previous code snippet tells Hibernate to execute the given SQL UPDATE statement instead of the default SQL DELETE statement. It changes the state of the account to DELETED and you can use the state property in all queries to exclude the deleted accounts.

That is all you need to do to create a basic soft delete implementation. But there are 2 other things you need to handle:

  1. When you delete an Account entity, Hibernate doesn’t update the value of its state attribute in the current session.
  2. You need to adapt all queries to exclude the deleted entities.

Update state property in current session

Hibernate doesn’t parse the native query you provide to the @SQLDelete annotation. It just sets the values of the bind parameters and executes it. It, therefore, doesn’t know that you provided an SQL UPDATE statement instead of a DELETE statement to the @SQLDelete annotation. It also doesn’t know that the value of the state attribute is outdated after it performed the delete operation.

In most cases, this is not an issue. As soon as Hibernate executes the SQL statement, the database record gets updated and all queries use the new state value. But what about the Account entity that you provided to the EntityManager.remove(Object entity) operation?

The state property of that entity is outdated. That’s not a big deal if you release the reference immediately after removing it. In all other cases, you should update the attribute yourself.

The easiest way to do that is to use a lifecycle callback, as I do in the following code snippet. The @PreRemove annotation on the deleteUser method tells Hibernate to call this method before it performs the remove operation. I use it to set the value of the state property to DELETED.

Exclude “deleted” entities in queries

You need to check the state attribute in all queries to exclude the deleted database records from the query results. This is an error-prone task if you do it manually and it forces you to define all queries yourself. The EntityManager.find(Class entityClass, Object primaryKey) method and the corresponding methods on the Hibernate Session don’t know about the semantic of the state attribute and don’t take it into account.

Hibernate’s @Where annotation provides a better way to exclude all deleted entities. It allows to define an SQL snippet which Hibernate adds to the WHERE clause of all queries. The following code snippet shows a @Where annotation that excludes a record if its state is DELETED.

As you can see in the following code snippets, Hibernate adds the defined WHERE clause when you perform a JPQL query or call the EntityManager.find(Class entityClass, Object primaryKey) method.


Summary

As you’ve seen, it’s quite simple to implement a soft delete with Hibernate. You just have to use a @SQLDelete annotation to define a custom SQL statement for the delete operation. You should also use Hibernate’s @Where annotation to define a predicate that excludes all deleted records by default.


Tags

Mapping, Query


About the author

Thorben is an independent consultant, international speaker, and trainer specialized in solving Java persistence problems with JPA and Hibernate.
He is also the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems.

Books and Courses

Coaching and Consulting

Leave a Repl​​​​​y

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.

  1. Great article, thank you!

    One question though – how should I handle errors when trying to (soft) delete something that is already marked as deleted? In `@SQLDelete` you added `check = ResultCheckStyle.COUNT`, but it looks like it never gets to this point because `@Where` filters out deleted items already?

    1. Hi Simon,

      my example doesn’t fail if you delete an already deleted entity.
      If you want to throw an exception in that situation, you need to change the @SQLDelete annotation like this:
      @SQLDelete(sql = “UPDATE account SET state = 'DELETED' WHERE id = ? AND state <> 'DELETED'”, check = ResultCheckStyle.COUNT)

      The ‘@Where’ annotation doesn’t affect the ‘@SQLDelete’ statement because it’s a native statement that Hibernate just executes and doesn’t parse.

      Regards,
      Thorben

  2. Great post!

    What if you need to load all entities? Using @Where I can’t anymore. As far as I know we can’t disable the @Where feature.

    For this kind of issue a more flexible solution would be to use Hibernate @Filter and @FilterDef or even Native Queries.

    What do you think?

  3. Thanks for sharing, I never knew this was possible.

    Can you do this in a Repository class by annotating the methods that you’re overriding? Or maybe even create custom repository methods and annotate them similar to what you did?

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}