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 javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

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.