java 131 lines · 4 tabs

Soft-Delete JPA Entities with Hibernate @Where and a Restore-Capable Repository

Shared by codesnips Sep 2026
4 tabs
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import java.time.Instant;

@MappedSuperclass
public abstract class SoftDeletable {

    @Column(nullable = false)
    private boolean deleted = false;

    @Column(name = "deleted_at")
    private Instant deletedAt;

    public void markDeleted() {
        this.deleted = true;
        this.deletedAt = Instant.now();
    }

    public void restore() {
        this.deleted = false;
        this.deletedAt = null;
    }

    public boolean isDeleted() {
        return deleted;
    }

    public Instant getDeletedAt() {
        return deletedAt;
    }
}
4 files · java Explain with highlit

Soft-deletion keeps rows in the database but hides them from normal queries by flagging them as deleted instead of issuing a DELETE. This preserves history, keeps foreign keys valid, and makes accidental removals recoverable. The trade-off is that every read must exclude deleted rows, which is easy to forget; the approach shown pushes that filtering down into the mapping layer so it happens automatically.

In SoftDeletable, a mapped superclass centralizes the deleted boolean and a deletedAt timestamp so every soft-deletable entity inherits the same columns and helper logic. The markDeleted and restore methods keep the flag and timestamp in sync, avoiding the bug where one is set without the other.

In Customer entity, the class extends SoftDeletable and is annotated with Hibernate's @Where(clause = "deleted = false"). That clause is appended to every query Hibernate generates for the entity — finder methods, JPQL, and lazy association loads all silently skip deleted rows. The @SQLDelete annotation rewrites the physical delete Hibernate emits so calling repository.delete(entity) runs an UPDATE ... SET deleted = true instead of removing the row, meaning existing delete call sites become soft deletes without changes.

Because @Where also hides deleted rows from the repository, restoring them needs a path that bypasses the filter. In CustomerRepository, a native @Query with nativeQuery = true selects by id while ignoring the clause, and a modifying @Modifying restoreById query flips the flag directly in SQL. Native queries are used deliberately: JPQL would inherit the @Where filter and never see the deleted row.

In CustomerService, softDelete delegates to the repository's overridden delete, while restore first loads the row through findDeletedById, guards against restoring something not actually deleted, then issues restoreById. The @Transactional boundary ensures the modifying query and any follow-up reads commit together.

The main pitfalls to remember: unique constraints still apply to soft-deleted rows, so a deleted column often belongs in partial unique indexes; and native restore queries skip Hibernate's dirty-checking, so the persistence context should be cleared or the entity reloaded afterward.


Related snips

Share this code

Here's the card — post it anywhere.

Soft-Delete JPA Entities with Hibernate @Where and a Restore-Capable Repository — share card
Link copied