How to implement a soft delete with Hibernate


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, 2 monthly Q&A calls, monthly coding challenges, a community of like-minded developers, and regular expert sessions.


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.

@Entity
@NamedQuery(name = “Account.FindByName”, query = “SELECT a FROM Account a WHERE name like :name”)
public class Account {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	@Column(name = “id”, updatable = false, nullable = false)
	private Long id;

	@Column
	private String name;

	@Column
	@Enumerated(EnumType.STRING)
	private AccountState state;

	…

}

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.

@Entity
@SQLDelete(sql = “UPDATE account SET state = ‘DELETED’ WHERE id = ?”, check = ResultCheckStyle.COUNT)
public class Account { … }

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.

Account a = em.find(Account.class, a.getId());
em.remove(a);
16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_0_, account0_.name as name2_0_0_, account0_.state as state3_0_0_ from Account account0_ where account0_.id=? and ( account0_.state <> ‘DELETED’)
16:07:59,534 DEBUG SQL:92 – UPDATE account SET state = ‘DELETED’ WHERE id = ?

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.

@Entity
@SQLDelete(sql = “UPDATE account SET state = ‘DELETED’ WHERE id = ?”, check = ResultCheckStyle.COUNT)
public class Account {

…
@PreRemove
public void deleteUser() {
this.state = AccountState.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.

@Entity
@SQLDelete(sql = “UPDATE account SET state = ‘DELETED’ WHERE id = ?”, check = ResultCheckStyle.COUNT)
@Where(clause = “state <> ‘DELETED'”)
@NamedQuery(name = “Account.FindByName”, query = “SELECT a FROM Account a WHERE name like :name”)
public class Account { … }

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.

TypedQuery<Account> q = em.createNamedQuery(“Account.FindByName”, Account.class);
q.setParameter(“name”, “%ans%”);
Account a = q.getSingleResult();

16:07:59,511 DEBUG SQL:92 – select account0_.id as id1_0_, account0_.name as name2_0_, account0_.state as state3_0_ from Account account0_ where ( account0_.state <> ‘DELETED’) and (account0_.name like ?)

Account a = em.find(Account.class, a.getId());

16:07:59,540 DEBUG SQL:92 – select account0_.id as id1_0_0_, account0_.name as name2_0_0_, account0_.state as state3_0_0_ from Account account0_ where account0_.id=? and ( account0_.state <> ‘DELETED’)

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.

Similar Posts

14 Comments

  1. Hmm…how to handle unique constraint values when entity is marked as deleted and new, inserted object should accept same unique value as deleted entity had?

    1. Thorben Janssen says:

      Hi Asko,
      I don’t know of any database that supports unique constraints over multiple columns. You either use a database trigger, which would be slower than the unique constraint, or you need to move the deleted record to a different table before you write the new one.
      Regards,
      Thorben

  2. 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. Thorben Janssen says:

      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

  3. Thank you. Great approach!

    1. Thorben Janssen says:

      Thanks Marin!

  4. Nice info. Thanks for sharing it

  5. Clement Nosariemen Ojo says:

    Many thanks for this post. This will solve a lot of problems for me. Never knew hibernate had this feature.

  6. Binh Thanh Nguyen says:

    Thanks, nice tips

  7. 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?

  8. Hatem Jaber says:

    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?

  9. Lenin Vladimir says:

    Wow! This is a thing I was asking myself how to do it best for years! Thanks!

    1. Thorben Janssen says:

      You’re welcome 🙂

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.