To delete data in Spring Boot with JPA and Hibernate, we may use the following ways

  • Use built-in Delete APIs of Spring Data JPA repositories

  • Use CascadeType.ALL or CascadeType.REMOVE attributes to delete the child entities when the parent entity is deleted. They may be used on @OneToOne, @OneToMany, @ManyToOne, and @ManyToMany

  • Use orphanRemoval attribute on @OneToMany or @OneToOne annotation to delete the child entities when they are removed from the relationship with the parent entity

  • Use Jakarta Persistence Query Language (JPQL) to delete a bulk of entities

  • Define status field in JPA and Hibernate entities to do a soft delete manually or with @Where and @SQLDelete

Using Delete APIs of Spring Data Repository

Spring Data Repository provides delete(entity) and deleteById(entityId) APIs to delete data with JPA and Hibernate entity

Suppose we define Customer JPA entity and Spring Data Repository as below

@Entity
public class Customer {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    ...
}
public interface CustomerRepository extends JpaRepository<Customer, Integer> {  
}

Then we can directly call the built-in Delete APIs of CustomerRepository to delete data

@Service
@Transactional
public class CustomerService {  
    @Autowired
    private CustomerRepository customerRepository;

    public void delete(Customer customer) {
        customerRepository.delete(customer);
    }

    public void deleteById(Integer id) {
        customerRepository.deleteById(id);
    }

    ...

}

Using CascadeType.ALL or CascadeType.REMOVE

The above delete methods work fine as long as the JPA and Hibernate entity does not have any relationship mapping. Otherwise, we may gain an SQL exception in the console when CascadeType.ALL or CascadeType.REMOVE is not specified in the mapping annotations

Suppose we map a one-to-many relationship between Customer and Card

@Entity
public class Customer {  
    ...

    @OneToMany(mappedBy = "customer")
    private Set<Card> cards;

    ...
}
@Entity
public class Card {

    ...

    @ManyToOne
    @JoinColumn
    private Customer customer;

    ...
}

Then when customerRepository.delete(customer) or customerRepository.deleteById(customerId) is triggered, the following error is expected

Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails ...

To fix that, we may use CascadeType.ALL or CascadeType.REMOVE attribute on @OneToOne, @OneToMany, @ManyToOne, and @ManyToMany mapping annotations to delete associated entities when the parent entity is deleted

@Entity
public class Customer {  
    ...

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
    private Set<Card> cards;

    ...
}

Using orphanRemoval

Using orphanRemoval attribute on @OneToMany or @OneToOne annotation to delete the child entities when they are removed from the relationship with the parent entity

Let's set orphanRemoval = true in the @OneToMany mapping between Customer and Card

@Entity
public class Customer {  
    ...

    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<Card> cards;

    ...
}
@Entity
public class Card {  
    public static final String VISA = "VISA";
    public static final String MASTER = "MASTER";

    ...

    private String type;

    @ManyToOne
    @JoinColumn
    private Customer customer;

    ...
}

Then when a card is removed from the associated collection, JPA and Hibernate would generate a corresponding DELETE SQL statement

customer.getCards().removeIf(c -> VISA.equals(c.getType()));  
customerService.save(customer);

Using JPQL to delete a bulk of entities

When using CaseCadeType.ALL or CaseCadeType.REMOVE to delete an entity along with its associated entities, we may find the following in the console

Hibernate: delete from card where card_number=?  
Hibernate: delete from card where card_number=?  
Hibernate: delete from customer where id=?

That says JPA and Hibernate generates one by one SQL DELETE statement for each associated entities. It works but not optimal. Obviously, we only need 1 SQL DELETE statement to delete all associated entities. Fortunately, we may use JPQL to achieve that

JPQL is a database-independent query language with the syntax and usage like SQL. JPQL works on JPA and Hibernate entities, while SQL works on database tables

Let's define 2 custom JPQL delete queries in CustomerRepository and CardRepository

public interface CustomerRepository extends JpaRepository<Customer, Integer> {  
    @Modifying
    @Query("DELETE Customer c WHERE c.id = ?1")
    void deleteByIdWithJPQL(int id);
}
public interface CardRepository extends JpaRepository<Card, Integer> {  
    @Modifying
    @Query("DELETE Card c WHERE c.customer.id = ?1")
    void deleteByCustomerId(int customerId);
}

Then call them to delete the associated entities along with the main entity

cardService.deleteByCustomerId(customer.getId());  
customerService.deleteByIdWithJPQL(customer.getId());

The main entity should be deleted last, otherwise, a SQL exception would be thrown as we violated the constraints

Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails ...

Soft delete manually

In practice, data is crucial to every application and system, so in most cases, instead of deleting we may like to archive it for analyzing and auditing later

To achieve that, we may add a flag or status field into the JPA and Hibernate entity. We would also need to add additional fields to track the created and updated date-time

@Entity
public class Customer {  
    ...

    @OneToMany(mappedBy = "customer")
    private Set<Card> cards;

    private Boolean deleted;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    ...
}
@Entity
public class Card {

    ...

    @ManyToOne
    @JoinColumn
    private Customer customer;

    private Boolean deleted;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

    ...
}

Then create a soft delete API to mark the flag field as deleted

@Service
@Transactional
public class CustomerService {  
    @Autowired
    private CustomerRepository customerRepository;

    public void softDelete(Integer id) {
        Customer customer = customerRepository.findById(id).get();
        customer.setDeleted(true);

        for(Card card : customer.getCards()) {
            card.setDeleted(true);
        }

        customerRepository.save(customer);
    }

    ...

}

@CreationTimestamp and @UpdateTimestamp are Hibernate annotations, available since Hibernate version 4.3

We also need to custom the Get or Find API to exclude the deleted entities

public interface CustomerRepository extends JpaRepository<Customer, Integer> {  
    ...

    List<Customer> findTop10ByDeletedFalse();
}

Soft delete with @Where and @SQLDelete

@Where and @SQLDelete are Hibernate annotations designed for soft deletion and defined on entities

We may use @Where(clause = ...) to exclude the entities with deleted status in all SELECT query and use @SQLDelete(sql = ...) to custom the default DELETE query generated by JPA and Hibernate

@SQLDelete(sql = "UPDATE Customer SET deleted = 1 WHERE id = ?")
@Where(clause = "deleted = 0")
@Entity
public class Customer {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    ...

    private Boolean deleted;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;
}
@SQLDelete(sql = "UPDATE Card SET deleted = 1 WHERE card_number = ?")
@Where(clause = "deleted = 0")
@Entity
public class Card {  
    @Id
    private Integer cardNumber;

    ...

    private Boolean deleted;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

When the Delete API of JPA and Hibernate are triggered

customerRepository.delete(customer);  
customerRepository.deleteById(1);

Hibernate would generate the queries defined by @SqlDelete instead of the default DELETE queries

Hibernate: UPDATE Card SET deleted = 1 WHERE card_number = ?  
Hibernate: UPDATE Customer SET deleted = 1 WHERE id = ?

When the Get or Find API of JPA and Hibernate are triggered

public interface CustomerRepository extends JpaRepository<Customer, Integer> {  
    ...

    List<Customer> findTop10By();

    @EntityGraph(attributePaths = "cards", type = EntityGraph.EntityGraphType.LOAD)
    List<Customer> findAllByIdIsIn(List<Integer> ids);
}
@Service
@Transactional
public class CustomerService {  
    ...

    public List<Customer> findTop10() {
        List<Customer> customerIds = customerRepository.findTop10By();

        return customerRepository.findAllByIdIsIn(
            customerIds.stream().map(Customer::getId).collect(Collectors.toList())
        );
    }
}

Hibernate would append the queries defined by the @Where annotation to include the entities with the undeleted status

Hibernate: select customer0_.id as id1_1, ...  
from customer customer0_  
where ( customer0_.deleted = 0)  
limit ?

Hibernate: select customer0_.id as id1_1_0_, ...  
from customer customer0_  
  left outer join card cards1_ on 
    customer0_.id=cards1_.customer_id 
    and ( cards1_.deleted = 0) 
where ( customer0_.deleted = 0)  
  and (customer0_.id in (? , ?))

Conclusion

In this tutorial, we learned various ways to delete data in JPA and Hibernate by using Spring Data Repository Delete APIs, CascadeType, orphanRemoval, JPQL, and soft delete. You may find the source code on GitHub