Envers Auditing
|
This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Three older reference books were consulted as bibliography only while preparing these pages and are not the
primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the
This section’s bibliography lists the reference material consulted while preparing these pages. |
Hibernate Envers records the full history of every change to an audited entity, not just the current row — distinct from the "who/when created or last modified" style of auditing Spring Data JPA provides (see below).
@Audited
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
@Entity
@Audited
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private BigDecimal price;
@ManyToOne
@Audited // associations need their own opt-in to be tracked in the audit history too
private Author author;
}
@Audited can be placed on the whole entity (every field audited) or on individual fields/associations only — an unaudited field simply is not tracked in history, even though the entity itself is.
The _AUD tables and REVINFO
For each audited entity’s table (book), Envers generates and maintains a shadow table (book_aud) with the
same columns plus a revision-number foreign key and a REVTYPE column (0 = add, 1 = modify, 2 = delete).
Every revision itself is a row in the REVINFO table (revision number, timestamp, by default) — one REVINFO
row per transaction that touched any audited entity, shared across every entity changed in that same
transaction. A new book_aud row is inserted (never updated) on every change, so the full history is a plain
SELECT … WHERE id = ? ORDER BY rev away even without the AuditReader API.
AuditReader queries
AuditReader reader = AuditReaderFactory.get(entityManager);
// the entity as of a specific past revision
Book bookAtRevision5 = reader.find(Book.class, bookId, 5);
// every revision number this entity was touched in
List<Number> revisions = reader.getRevisions(Book.class, bookId);
// a full audit query -- e.g. every price change above a threshold, across all books
List<Object[]> priceHikes = reader.createQuery()
.forRevisionsOfEntity(Book.class, false, true)
.add(AuditEntity.property("price").gt(BigDecimal.valueOf(100)))
.getResultList();
AuditQuery (via createQuery().forRevisionsOfEntity(…)) supports a subset of Criteria-style predicates
against the historical data, letting audit queries filter by property value, revision type, or revision
metadata without hand-writing SQL against the _aud tables.
Custom revision entities
The default REVINFO table holds only a revision number and timestamp. A custom revision entity adds
application-specific context (who made the change, from which request) to every revision:
@Entity
@RevisionEntity
public class ExtendedRevisionEntity {
@Id
@GeneratedValue
@RevisionNumber
private int id;
@RevisionTimestamp
private long timestamp;
private String changedBy; // populated via a RevisionListener
}
A org.hibernate.envers.RevisionListener implementation, registered on the @RevisionEntity, is invoked once
per new revision and populates any extra fields (e.g. from the current security context) before the revision
row is flushed.
ValidityAuditStrategy vs. the default
The default audit strategy stores only each revision’s own state — finding "what was the state at time T"
requires locating the latest revision at or before T. ValidityAuditStrategy additionally stores, on each
_aud row, the revision at which that row’s validity ended (a REVEND column) — turning "state as of time
T" into a direct range query instead of a latest-before scan, at the cost of an extra UPDATE to the previous
_aud row every time a new revision is inserted. Reach for ValidityAuditStrategy when point-in-time queries
against audit history are frequent and query performance on large audit tables matters; the default strategy is
simpler and cheaper to write, appropriate when audit history is mostly write-and-forget (retained for
compliance, rarely queried).
Conditional auditing
Not every change necessarily deserves an audit trail entry. Conditional auditing is achieved by wrapping the
persistence operation in application logic that decides whether to include the change in the current audited
transaction at all — Envers itself has no per-write "skip this one" flag; the usual pattern is a dedicated,
unaudited entity/table for changes that should never appear in history (system-internal housekeeping updates),
keeping the @Audited entity’s writes limited to changes that are meant to be part of the record.
Differs from Spring Data’s own auditing
Spring Data JPA’s @CreatedDate/@LastModifiedDate/@CreatedBy/@LastModifiedBy (see
Spring Data JPA — Auditing) records only the current row’s creation/last-modification metadata — four extra columns on the
entity’s own table, no history of intermediate states. Envers records every intermediate state as its own
row in a separate _aud table — a genuinely different capability (full history vs. current-state metadata),
and the two compose without conflict: an entity can carry both @Audited and Spring Data’s auditing
annotations at once.