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:
- tell Hibernate to perform a SQL UPDATE instead of a DELETE operation and
- 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:
- When you delete an Account entity, Hibernate doesn’t update the value of its state attribute in the current session.
- 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.
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.