java 127 lines · 3 tabs

Soft-Delete JPA Entities with Hibernate @SQLDelete and @Where Filtering

Shared by codesnips Aug 2026
3 tabs
package com.example.docs.domain;

import jakarta.persistence.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import java.time.Instant;

@Entity
@Table(name = "documents")
@SQLDelete(sql = "UPDATE documents SET deleted = true, deleted_at = now() WHERE id = ?")
@Where(clause = "deleted = false")
public class Document {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(columnDefinition = "text")
    private String body;

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

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

    @Column(name = "deleted_by")
    private String deletedBy;

    protected Document() {
    }

    public Document(String title, String body) {
        this.title = title;
        this.body = body;
    }

    public void markDeletedBy(String actor) {
        this.deletedBy = actor;
    }

    public Long getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }

    public boolean isDeleted() {
        return deleted;
    }
}
3 files · java Explain with highlit

Soft deletion keeps rows in the database after a logical delete so history, audits, and foreign-key references stay intact, while application queries behave as if the record is gone. This snippet shows the idiomatic Hibernate way to implement it without scattering WHERE deleted = false across every query.

In Document entity, the class carries a deleted boolean plus deletedAt and deletedBy audit columns. The @SQLDelete annotation rewrites the SQL that Hibernate emits when an entity is removed: instead of issuing DELETE FROM documents, it runs an UPDATE that flips the flag and stamps the timestamp. The companion @Where(clause = "deleted = false") is applied to every SELECT Hibernate generates for the entity, so a normal findById or JPQL query silently excludes archived rows. Together these two annotations make repository.delete(doc) and a fetch feel exactly like a hard delete, which is why callers need no special-casing.

The key trade-off is that @Where is a static filter baked into the mapping — it cannot be turned off per query, so retrieving archived rows requires a native query or a dedicated method that bypasses the managed entity. It also does not apply to @ManyToOne eager loads through the second-level cache identically across all Hibernate versions, so lazy associations are the safer default.

In DocumentRepository, restore and findArchived are declared with @Query(nativeQuery = true) precisely because @Where would otherwise hide the very rows these methods need to see. The native UPDATE in restore is marked @Modifying and clears the persistence context so stale managed instances are not returned afterward.

In DocumentService, archive loads the entity, records who performed the action, and lets deleteById trigger the @SQLDelete statement inside a transaction. restore and listArchived expose the escape hatches. This layering keeps the soft-delete mechanics in the mapping while the service owns the audit intent, which is exactly where a developer reaches for this pattern: entities that must never truly disappear but should stay invisible to ordinary reads.


Related snips

Share this code

Here's the card — post it anywhere.

Soft-Delete JPA Entities with Hibernate @SQLDelete and @Where Filtering — share card
Link copied