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, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


In some applications, you don’t want to or are not allowed to remove a record from the database permanently. But you still need to remove or hide records which are no longer active. One example could be a user account you want to keep because it is linked to other business objects that 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 explained the audit log option in my articles about Hibernate Envers. 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.

Adding such a field is, obviously, just the 1st step to implementing the soft delete feature. You also have to set it when persisting a new record, and instead of deleting a record, you must change its soft delete indicator. To hide the soft deleted records from your users, you also have to adjust all queries to exclude records based on their soft delete indicator.

This might sound like a lot of work, but it isn’t if you’re using Hibernate.

How to implement a soft delete with Hibernate

In version 6.4, the Hibernate team introduced an official soft delete feature for Hibernate ORM. It now only takes 1 annotation to activate soft deletes for an entity class. Hibernate then generates the required SQL UPDATE statement for soft deleting a record and adjusts all query statements to exclude soft deleted records. I will show you how to activate soft deletes and your different configuration options in the next section of this article.

For Hibernate versions <=6.3, you have to implement the soft delete feature yourself. This requires a little bit of extra work. But don’t worry, it’s not complicated and you can declare all required parts in your entity mappings. So you don’t have to handle it in your business code. I will show you how to implement the mapping at the end of this article.

Soft delete with Hibernate >= 6.4

Hibernate 6.4 introduced support for soft deletes. You only need to annotate your entity class with a @SoftDelete annotation and Hibernate handles the rest.

Hibernates default soft delete implementation

The following code snippet uses Hibernate’s default soft delete implementation. It expects a deleted column of type boolean in the underlying database table. You can use the columnName attribute of the @SoftDelete annotation to specify a different name for that column.

@Entity
@SoftDelete
public class Account { ... }

Hibernate automatically sets the deleted field to false when you persist a new Account entity.

Account a = new Account();
a.setName("thjanssen");
em.persist(a);
10:30:49,099 DEBUG SQL:135 - insert into Account (name,deleted,id) values (?,false,?)

When you remove the entity, Hibernate sets the deleted field to true. And Hibernate adds a predicate that excludes all records where deleted=true to all JPQL and Criteria queries and all generated statements.

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

TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();
10:30:49,199 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.deleted=false and a1_0.id=?
10:30:49,211 DEBUG SQL:135 - update Account set deleted=true where id=? and deleted=false

10:30:49,234 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.active=true

Using a different SoftDeleteType

Hibernate supports 2 different SoftDeleteTypes that determine the meaning of the boolean stored in the database:

  1. SoftDeleteType.ACTIVE
    The value true marks a record as active, and Hibernate uses active as the default column name.
  2. SoftDeleteType.DELETED
    The value true marks a record as deleted, and Hibernate uses deleted as the default column name. This is the default.

Here, you can see a simple mapping that sets the SoftDeleteType to ACTIVE.

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE)
public class Account { ... }

When I now execute the same tests as before, Hibernate stores the entity’s current state in the active column. When I remove the entity, Hibernate sets that field to false, and all queries only return records where active=true.

// persist a new Account
Account a = new Account();
a.setName("thjanssen");
em.persist(a);

// find and remove an Account
a = em.find(Account.class, a.getId());
em.remove(a);

// query an Account
TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();
// persist a new Account
10:46:26,099 DEBUG SQL:135 - insert into Account (name,active,id) values (?,true,?)

// find and remove an Account
10:46:26,199 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.active=true and a1_0.id=?
10:46:26,211 DEBUG SQL:135 - update Account set active=false where id=? and active=true

// query an Account
10:46:26,234 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.active=true

Using a different column type

Hibernate’s default soft delete implementation uses a column of type boolean to store the current state of each record. You can change that by providing an AttributeConverter that maps the internally used Boolean to a database type of your choice.

This is useful if you’re working with a legacy database. These sometimes use a String or an enum representing each record’s current state.

Here, you can see an entity mapping telling Hibernate to store the state of the current record in the state column and use the StateConverter AttributeConverter to map the attribute.

@Entity
@SoftDelete(strategy = SoftDeleteType.ACTIVE, columnName = "state", converter = StateConverter.class)
public class Account { ... }

The StateConverter implementation is straightforward. It implements the AttributeConverter interface with its convertToDatabaseColumn and convertToEntityAttribute methods.

Hibernate’s soft delete implementation relies on a Boolean indicating if a record is active or deleted. Due to that, the 1st type parameter of the AttributeConverter has to be a Boolean. The 2nd type parameter specifies the type you want to store in the database. In this example, I want to map the Boolean to the String “active” or “inactive”.

public class StateConverter implements AttributeConverter<Boolean, String>{

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return attribute ? "active" : "inactive";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return dbData.equals("active");
    }
    
}

WARNING: If you map the Boolean to a String, Hibernate’s schema generation creates a varchar column of length 1. If your String is longer than 1 character, I recommend providing your own database migration.

When I now execute the same test as before, Hibernate stores the values “active” or “inactive” in the state column. When I remove the entity, Hibernate sets that field to “inactive”, and all queries only return records where state=active.

// persist a new Account
Account a = new Account();
a.setName("thjanssen");
em.persist(a);

// find and remove an Account
a = em.find(Account.class, a.getId());
em.remove(a);

// query an Account
TypedQuery<Account> q = em.createNamedQuery("Account.FindByName", Account.class);
q.setParameter("name", "%ans%");
a = (Account) q.getSingleResult();
// persist a new Account
10:46:26,099 DEBUG SQL:135 - insert into Account (name,state,id) values (?,'active',?)

// find and remove an Account
11:18:39,857 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.state='active' and a1_0.id=?
11:18:39,867 DEBUG SQL:135 - update Account set state='inactive' where id=? and state='active'

// query an Account
11:18:39,889 DEBUG SQL:135 - select a1_0.id,a1_0.name from Account a1_0 where a1_0.name like ? escape '' and a1_0.state='active'

Soft delete with Hibernate < 6.4

It’s not that difficult to implement a soft delete with Hibernate <6.4, but it requires a little bit of extra work. You have to:

  1. tell Hibernate to perform an SQL UPDATE instead of a DELETE operation when removing an entity object and
  2. exclude all soft deleted records from your query results.

I’ll show you how you can easily do that in the following examples. All of them will use the following Account entity, which uses the AccountState state attribute 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 performs the delete operation.

In most cases, this is not an issue. When Hibernate executes the SQL statement, the database record gets updated, and all queries use the new state value. But what about the Account entity 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 soft 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 semantics 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 the definition of 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.

If you’re using a Hibernate version >= 6.4, you only have to annotate your entity class with @SoftDelete. Hibernate then automatically manages the current state of each record and adapts all queries to exclude soft deleted records.

If you’re using a Hibernate <= 6.3, you have to implement soft deletes yourself. You do that by annotating your entity class with a @SQLDelete annotation and providing an SQL UPDATE statement to change the record’s state. You should also use Hibernate’s @Where annotation to define a predicate that excludes all soft 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.