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;
}
}
package com.example.docs.repository;
import com.example.docs.domain.Document;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
public interface DocumentRepository extends JpaRepository<Document, Long> {
Optional<Document> findByTitle(String title);
// Native query bypasses @Where so archived rows are visible again.
@Modifying(clearAutomatically = true)
@Query(value = "UPDATE documents SET deleted = false, deleted_at = null, "
+ "deleted_by = null WHERE id = :id", nativeQuery = true)
int restore(@Param("id") Long id);
@Query(value = "SELECT * FROM documents WHERE deleted = true "
+ "ORDER BY deleted_at DESC", nativeQuery = true)
List<Document> findArchived();
@Query(value = "SELECT * FROM documents WHERE id = :id AND deleted = true",
nativeQuery = true)
Optional<Document> findArchivedById(@Param("id") Long id);
}
package com.example.docs.service;
import com.example.docs.domain.Document;
import com.example.docs.repository.DocumentRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class DocumentService {
private final DocumentRepository documents;
public DocumentService(DocumentRepository documents) {
this.documents = documents;
}
@Transactional
public void archive(Long id, String actor) {
Document doc = documents.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Document " + id));
doc.markDeletedBy(actor);
documents.flush(); // persist deleted_by before the @SQLDelete update
documents.deleteById(id); // fires UPDATE ... SET deleted = true
}
@Transactional
public void restore(Long id) {
int rows = documents.restore(id);
if (rows == 0) {
throw new EntityNotFoundException("No archived document " + id);
}
}
@Transactional(readOnly = true)
public List<Document> listArchived() {
return documents.findArchived();
}
}
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
import os
import stat
for root, _dirs, files in os.walk('/etc'):
for name in files:
path = os.path.join(root, name)
Python security audit script for exposed risky filesystem state
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
Share this code
Here's the card — post it anywhere.