Hibernate - Using @SoftDelete Annotation Examples

This tutorial shows you how to use Hibernate's @SoftDelete annotation with examples.

A row that's inserted into a database can be deleted later for some reason. In many cases, we want to keep the deleted rows in the database by adding a column that indicates whether the row is deleted or not. That's also known as soft delete. If you use Hibernate version 6.4 or above, implementing soft delete should become easier since you can use the @SoftDelete annotation. Below are the examples of how to use the annotation.

Using @SoftDelete Annotation

The @SoftDelete annotation can be placed on various levels which include package, type, field, method, and annotation type. It has some parameters but none of them are required. One of the parameters is strategy which is used to set the strategy how to indicate that a value in a column means the row is deleted. It accepts an enum whose values are ACTIVE and DELETED. The default value if you don't pass the parameter is DELETED. The differences of those two values can be seen on the table below.

Value True False
ACTIVE Row is active (non-deleted) Row is inactive (deleted)
DELETED Row is inactive (deleted) Row is active (non-deleted)

Another parameter that you need to know is columnName which refers to the column that indicates whether the value is active or not. The default value depends on the strategy used. If the strategy is DELETED, the default column name is deleted. If the strategy is ACTIVE, the default column name is active. To define a different column to use, you can pass a columnName value to the annotation.

If the value type stored in the database is not a boolean, you may also need to pass a custom AttributeConverter as the converter argument. The converter needs to handle conversions between the entity attribute type (boolean) and the database type.

Usage on Type/Class

For example, we have an entity type named Book and we want to use soft deletion for it. To indicate that a row is deleted, there is a field named isRemoved. If the value of isRemoved is true, the row is deleted. Otherwise, the row is non-deleted. For that purpose, we can annotate the class with @SoftDelete annotation. Since the column name is different from the default name (deleted), we need to pass the columnName to the annotation.

  @SoftDelete(columnName = "isRemoved")
  public class Book {

    @Id
    @UuidGenerator
    private UUID id;

    private String title;

    private String author;

    private boolean isRemoved;
  }

We can test it by adding a code to remove a row.

  public class BookService {
  
    private final EntityManager entityManager;
    
    public BookService(EntityManager entityManager) {
      this.entityManager = entityManager;
    }
  
    @Transactional
    public void deleteBook(UUID id) {
      Book book = this.entityManager.find(Book.class, id);
  
      if (book == null) {
        return;
      }
  
      this.entityManager.remove(book);
    }
  }

Below is the output when entityManager.remove is invoked.

  org.hibernate.SQL           : update books set is_removed=true where id=? and is_removed=false
  org.hibernate.orm.jdbc.bind : binding parameter (1:UUID) <- [877a4ef2-7e18-4a77-8e53-b2b6f78b525a]

As you can read, Hibernate will generate an update query instead of a delete query. For the case above, to perform a deletion, the generated query sets the is_removed value to true. In addition, it automatically adds is_removed=false in the criteria.

Not only for update queries, Hibernate also adds the is_removed=false criteria for select queries.

  public class BookService {
  
    private final EntityManager entityManager;
  
    public BookService(EntityManager entityManager) {
      this.entityManager = entityManager;
    }
  
  
    public List<BookDto> fetchBooks() {
      List<Book> books = this.entityManager.createQuery("SELECT b FROM Book b", Book.class)
          .getResultList();
  
      return books.stream()
        .map(this::mapToBookDto)
        .collect(Collectors.toList());
    }
  
    private BookDto mapToBookDto(Book book) {
      return BookDto.builder()
        .id(book.getId())
        .title(book.getTitle())
        .author(book.getAuthor())
        .isDeleted(book.isRemoved())
        .build();
    }
  
    // other methods
  }

Below is the output when the select query above is executed.

  org.hibernate.SQL           : select b1_0.id,b1_0.author,b1_0.is_removed,b1_0.title from books b1_0 where b1_0.is_removed=false

The annotation makes it much simpler to have a soft delete feature since Hibernate automatically adds the criteria to filter out soft-deleted rows. That also reduces the chance of forgetting to add the criteria in some queries.

Usage on Field

The annotation can be defined on field or method level. It can be applied to the table of @ElementCollection or @ManyToMany.

For example, we have an element collection named tags. If it's annotated with @SoftDelete, the table must have a column that marks whether it's deleted or not.

  public class Book {

    @ElementCollection
    @SoftDelete(columnName = "isDeleted")
    private List<String> tags;

    // other fields
  }

Below is a query that fetches the collection.

  List<Book> books = this.entityManager.createQuery("SELECT b FROM Book b JOIN FETCH b.tags", Book.class)
    .getResultList();

From the output below, we can see that Hibernate adds the criteria to filter the rows from the element collection table.

  org.hibernate.SQL           : select b1_0.id,b1_0.author,b1_0.is_removed,t1_0.book_id,t1_0.tags,b1_0.title from books b1_0 left join book_tags t1_0 on b1_0.id=t1_0.book_id and t1_0.is_deleted=false where b1_0.is_removed=false

Usage on Package

If the annotation is defined on the package-level, it will have any effect on all entities in the package. This can be useful if you want to implement soft delete to all entities in a package. If you want to use a different soft delete config for an entity, you can define another annotation for the entity. Hibernate will use the more specific one.

  @SoftDelete(columnName = "isRemoved")
  package com.woolha.hibernate6.example.model;

Summary

Hibernate's @SoftDelete annotation makes it easier for us to have entities that support soft delete. First, make sure the database table must have a column that indicates whether the row is deleted or not. You just need to add the annotation which can be put on package, type, field, method, or annotation type level. When Hibernate executes an operation to remove a row, it will generate an update query that sets the value of the column that's used for soft-deletion. In addition, the annotation also affects select, update, and delete queries for which Hibernate adds the criteria to filter out soft-deleted rows.

You can also read about: