Auditing, Soft Deletes and Versioning in JPA
Auditing, Soft Deletes and Versioning in JPA
Knowing who changed a record and when is a common requirement in regulated industries. JPA provides built-in auditing with @CreatedDate, @LastModifiedDate, and @Version. Combined with soft deletes, you get a complete history of your data without ever losing a row.
Enabling JPA Auditing
Spring Data JPA makes auditing straightforward. Annotate a base class with audit fields, enable auditing in configuration, and Spring handles the rest:
import jakarta.persistence.*;
import org.springframework.data.annotation.*;
import java.time.LocalDateTime;
@MappedSuperclass
public abstract class Auditable {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String modifiedBy;
// getters and setters
}
@Entity
@Table(name = "products")
public class Product extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
// getters and setters
}
You also need an auditor provider. The simplest approach is to implement AuditorAware<String> that returns the current user from the security context. Spring will inject the correct values automatically on persist and update.
Soft Deletes with @SQLDelete
Soft deletes mark a row as deleted without removing it. Hibernate does not support this natively, but the @SQLDelete and @Where annotations from Hibernate Extras let you override the DELETE statement and filter out soft-deleted rows from queries:
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
@Entity
@Table(name = "orders")
@SQLDelete(sql = "UPDATE orders SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String product;
private double total;
private boolean deleted;
// getters and setters
}
Now when you call entityManager.remove(order), Hibernate generates UPDATE orders SET deleted = true WHERE id = ? instead of DELETE. All normal queries automatically exclude deleted rows because of the @Where clause. You can still query deleted rows by using a native SQL query if you need an admin audit view.
Version-Based Conflict Detection
The @Version annotation adds a version column that Hibernate increments on every UPDATE. This serves double duty: it detects optimistic locking conflicts and also acts as an audit trail of how many times a row has changed:
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Version
private long version;
// getters and setters
}
Every UPDATE sets version = version + 1. If two transactions try to update the same row, the second one fails with OptimisticLockException because the version no longer matches. This prevents lost updates without holding any database locks.
Audit Table Pattern
For full change history, create a separate audit table that stores every version of a row. Use Hibernate event listeners or a library like Envers to capture INSERT, UPDATE, and DELETE events and write them to the audit table with timestamps and user info.
Key Points
- Extend
Auditablewith@CreatedDateand@LastModifiedDatefor automatic timestamps. - Implement
AuditorAwareto track which user performed each change. - Soft deletes use
@SQLDeleteto turn DELETE into UPDATE and@Whereto filter results. @Versionprovides optimistic locking and tracks the number of changes per row.- Audit tables or Envers can store full change history for regulatory compliance.