21. Envers

21.1. Basics

To audit changes that are performed on an entity, you only need two things:

  • the hibernate-envers jar on the classpath,

  • an @Audited annotation on the entity.

Unlike in previous versions, you no longer need to specify listeners in the Hibernate configuration file. Just putting the Envers jar on the classpath is enough because listeners will be registered automatically.

And that’s all. You can create, modify and delete the entities as always.

The use of JPA’s CriteriaUpdate and CriteriaDelete bulk operations are not currently supported by Envers due to how an entity’s lifecycle events are dispatched. Such operations should be avoided as they’re not captured by Envers and lead to incomplete audit history.

If you look at the generated schema for your entities, or at the data persisted by Hibernate, you will notice that there are no changes. However, for each audited entity, a new table is introduced - entity_table_AUD, which stores the historical data, whenever you commit a transaction.

Envers automatically creates audit tables if hibernate.hbm2ddl.auto option is set to create, create-drop or update. Appropriate DDL statements can also be generated with an Ant task in Generating Envers schema with Hibernate hbm2ddl tool.

Considering we have a Customer entity, when annotating it with the Audited annotation, Hibernate is going to generate the following tables using the hibernate.hbm2ddl.auto schema tool:

Example 652. Basic Envers entity mapping

  1. @Audited
  2. @Entity(name = "Customer")
  3. public static class Customer {
  4. @Id
  5. private Long id;
  6. private String firstName;
  7. private String lastName;
  8. @Temporal( TemporalType.TIMESTAMP )
  9. @Column(name = "created_on")
  10. @CreationTimestamp
  11. private Date createdOn;
  12. //Getters and setters are omitted for brevity
  13. }
  1. create table Customer (
  2. id bigint not null,
  3. created_on timestamp,
  4. firstName varchar(255),
  5. lastName varchar(255),
  6. primary key (id)
  7. )
  8. create table Customer_AUD (
  9. id bigint not null,
  10. REV integer not null,
  11. REVTYPE tinyint,
  12. created_on timestamp,
  13. firstName varchar(255),
  14. lastName varchar(255),
  15. primary key (id, REV)
  16. )
  17. create table REVINFO (
  18. REV integer generated by default as identity,
  19. REVTSTMP bigint,
  20. primary key (REV)
  21. )
  22. alter table Customer_AUD
  23. add constraint FK5ecvi1a0ykunrriib7j28vpdj
  24. foreign key (REV)
  25. references REVINFO

Instead of annotating the whole class and auditing all properties, you can annotate only some persistent properties with @Audited. This will cause only these properties to be audited.

Now, considering the previous Customer entity, let’s see how Envers auditing works when inserting, updating, and deleting the entity in question.

Example 653. Auditing the entity INSERT operation

  1. Customer customer = new Customer();
  2. customer.setId( 1L );
  3. customer.setFirstName( "John" );
  4. customer.setLastName( "Doe" );
  5. entityManager.persist( customer );
  1. insert
  2. into
  3. Customer
  4. (created_on, firstName, lastName, id)
  5. values
  6. (?, ?, ?, ?)
  7. -- binding parameter [1] as [TIMESTAMP] - [Mon Jul 24 17:21:32 EEST 2017]
  8. -- binding parameter [2] as [VARCHAR] - [John]
  9. -- binding parameter [3] as [VARCHAR] - [Doe]
  10. -- binding parameter [4] as [BIGINT] - [1]
  11. insert
  12. into
  13. REVINFO
  14. (REV, REVTSTMP)
  15. values
  16. (?, ?)
  17. -- binding parameter [1] as [BIGINT] - [1]
  18. -- binding parameter [2] as [BIGINT] - [1500906092803]
  19. insert
  20. into
  21. Customer_AUD
  22. (REVTYPE, created_on, firstName, lastName, id, REV)
  23. values
  24. (?, ?, ?, ?, ?, ?)
  25. -- binding parameter [1] as [INTEGER] - [0]
  26. -- binding parameter [2] as [TIMESTAMP] - [Mon Jul 24 17:21:32 EEST 2017]
  27. -- binding parameter [3] as [VARCHAR] - [John]
  28. -- binding parameter [4] as [VARCHAR] - [Doe]
  29. -- binding parameter [5] as [BIGINT] - [1]
  30. -- binding parameter [6] as [INTEGER] - [1]

Example 654. Auditing the entity UPDATE operation

  1. Customer customer = entityManager.find( Customer.class, 1L );
  2. customer.setLastName( "Doe Jr." );
  1. update
  2. Customer
  3. set
  4. created_on=?,
  5. firstName=?,
  6. lastName=?
  7. where
  8. id=?
  9. -- binding parameter [1] as [TIMESTAMP] - [2017-07-24 17:21:32.757]
  10. -- binding parameter [2] as [VARCHAR] - [John]
  11. -- binding parameter [3] as [VARCHAR] - [Doe Jr.]
  12. -- binding parameter [4] as [BIGINT] - [1]
  13. insert
  14. into
  15. REVINFO
  16. (REV, REVTSTMP)
  17. values
  18. (?, ?)
  19. -- binding parameter [1] as [BIGINT] - [2]
  20. -- binding parameter [2] as [BIGINT] - [1500906092853]
  21. insert
  22. into
  23. Customer_AUD
  24. (REVTYPE, created_on, firstName, lastName, id, REV)
  25. values
  26. (?, ?, ?, ?, ?, ?)
  27. -- binding parameter [1] as [INTEGER] - [1]
  28. -- binding parameter [2] as [TIMESTAMP] - [2017-07-24 17:21:32.757]
  29. -- binding parameter [3] as [VARCHAR] - [John]
  30. -- binding parameter [4] as [VARCHAR] - [Doe Jr.]
  31. -- binding parameter [5] as [BIGINT] - [1]
  32. -- binding parameter [6] as [INTEGER] - [2]

Example 655. Auditing the entity DELETE operation

  1. Customer customer = entityManager.getReference( Customer.class, 1L );
  2. entityManager.remove( customer );
  1. delete
  2. from
  3. Customer
  4. where
  5. id = ?
  6. -- binding parameter [1] as [BIGINT] - [1]
  7. insert
  8. into
  9. REVINFO
  10. (REV, REVTSTMP)
  11. values
  12. (?, ?)
  13. -- binding parameter [1] as [BIGINT] - [3]
  14. -- binding parameter [2] as [BIGINT] - [1500906092876]
  15. insert
  16. into
  17. Customer_AUD
  18. (REVTYPE, created_on, firstName, lastName, id, REV)
  19. values
  20. (?, ?, ?, ?, ?, ?)
  21. -- binding parameter [1] as [INTEGER] - [2]
  22. -- binding parameter [2] as [TIMESTAMP] - [null]
  23. -- binding parameter [3] as [VARCHAR] - [null]
  24. -- binding parameter [4] as [VARCHAR] - [null]
  25. -- binding parameter [5] as [BIGINT] - [1]
  26. -- binding parameter [6] as [INTEGER] - [3]

The REVTYPE column value is taken from the RevisionType Enum.

Table 11. REVTYPE column values

Database column value

Associated RevisionType Enum value

Description

0

ADD

A database table row was inserted.

1

MOD

A database table row was updated.

2

DEL

A database table row was deleted.

The audit (history) of an entity can be accessed using the AuditReader interface, which can be obtained by having an open EntityManager or Session via the AuditReaderFactory.

Example 656. Getting a list of revisions for the Customer entity

  1. List<Number> revisions = doInJPA( this::entityManagerFactory, entityManager -> {
  2. return AuditReaderFactory.get( entityManager ).getRevisions(
  3. Customer.class,
  4. 1L
  5. );
  6. } );
  1. select
  2. c.REV as col_0_0_
  3. from
  4. Customer_AUD c
  5. cross join
  6. REVINFO r
  7. where
  8. c.id = ?
  9. and c.REV = r.REV
  10. order by
  11. c.REV asc
  12. -- binding parameter [1] as [BIGINT] - [1]

Using the previously fetched revisions, we can now inspect the state of the Customer entity at that particular revision:

Example 657. Getting the first revision for the Customer entity

  1. Customer customer = (Customer) AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, revisions.get( 0 ) )
  5. .getSingleResult();
  6. assertEquals("Doe", customer.getLastName());
  1. select
  2. c.id as id1_1_,
  3. c.REV as REV2_1_,
  4. c.REVTYPE as REVTYPE3_1_,
  5. c.created_on as created_4_1_,
  6. c.firstName as firstNam5_1_,
  7. c.lastName as lastName6_1_
  8. from
  9. Customer_AUD c
  10. where
  11. c.REV = (
  12. select
  13. max( c_max.REV )
  14. from
  15. Customer_AUD c_max
  16. where
  17. c_max.REV <= ?
  18. and c.id = c_max.id
  19. )
  20. and c.REVTYPE <> ?
  21. -- binding parameter [1] as [INTEGER] - [1]
  22. -- binding parameter [2] as [INTEGER] - [2]

When executing the aforementioned SQL query, there are two parameters:

revision_number

The first parameter marks the revision number we are interested in or the latest one that exists up to this particular revision.

revision_type

The second parameter specifies that we are not interested in DEL RevisionType so that deleted entries are filtered out.

The same goes for the second revision associated with the UPDATE statement.

Example 658. Getting the second revision for the Customer entity

  1. Customer customer = (Customer) AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, revisions.get( 1 ) )
  5. .getSingleResult();
  6. assertEquals("Doe Jr.", customer.getLastName());

For the deleted entity revision, Envers throws a NoResultException since the entity was no longer valid at that revision.

Example 659. Getting the third revision for the Customer entity

  1. try {
  2. Customer customer = (Customer) AuditReaderFactory
  3. .get( entityManager )
  4. .createQuery()
  5. .forEntitiesAtRevision( Customer.class, revisions.get( 2 ) )
  6. .getSingleResult();
  7. fail("The Customer was deleted at this revision: " + revisions.get( 2 ));
  8. }
  9. catch (NoResultException expected) {
  10. }

You can use the forEntitiesAtRevision(Class<T> cls, String entityName, Number revision, boolean includeDeletions) method to get the deleted entity revision so that, instead of a NoResultException, all attributes, except for the entity identifier, are going to be null.

Example 660. Getting the third revision for the Customer entity without getting a NoResultException

  1. Customer customer = (Customer) AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision(
  5. Customer.class,
  6. Customer.class.getName(),
  7. revisions.get( 2 ),
  8. true )
  9. .getSingleResult();
  10. assertEquals( Long.valueOf( 1L ), customer.getId() );
  11. assertNull( customer.getFirstName() );
  12. assertNull( customer.getLastName() );
  13. assertNull( customer.getCreatedOn() );

See the Javadocs for details on other functionality offered.

21.2. Configuration Properties

It is possible to configure various aspects of Hibernate Envers behavior, such as table names, etc.

**org.hibernate.envers.audit_table_prefix**

String that will be prepended to the name of an audited entity to create the name of the entity and that will hold audit information.

**org.hibernate.envers.audit_table_suffix** (default: _AUD)

String that will be appended to the name of an audited entity to create the name of the entity and that will hold audit information.

If you audit an entity with a table name Person, in the default setting Envers will generate a Person_AUD table to store historical data.

**org.hibernate.envers.revision_field_name** (default: REV)

Name of a field in the audit entity that will hold the revision number.

**org.hibernate.envers.revision_type_field_name** (default: REVTYPE )

Name of a field in the audit entity that will hold the type of the revision (currently, this can be: add, mod, del).

**org.hibernate.envers.revision_on_collection_change** (default: true )

Should a revision be generated when a not-owned relation field changes (this can be either a collection in a one-to-many relation or the field using mappedBy attribute in a one-to-one relation).

**org.hibernate.envers.do_not_audit_optimistic_locking_field** (default: true )

When true, properties to be used for optimistic locking, annotated with @Version, will not be automatically audited (their history won’t be stored; it normally doesn’t make sense to store it).

**org.hibernate.envers.store_data_at_delete** (default: false )

Should the entity data be stored in the revision when the entity is deleted (instead of only storing the id and all other properties as null).

This is not normally needed, as the data is present in the last-but-one revision. Sometimes, however, it is easier and more efficient to access it in the last revision (then the data that the entity contained before deletion is stored twice).

**org.hibernate.envers.default_schema** (default: null - same schema as the table being audited)

The default schema name that should be used for audit tables.

Can be overridden using the @AuditTable( schema = "…​" ) annotation.

If not present, the schema will be the same as the schema of the table being audited.

**org.hibernate.envers.default_catalog** (default: null - same catalog as the table being audited)

The default catalog name that should be used for audit tables.

Can be overridden using the @AuditTable( catalog = "…​" ) annotation.

If not present, the catalog will be the same as the catalog of the normal tables.

**org.hibernate.envers.audit_strategy**(default: org.hibernate.envers.strategy.DefaultAuditStrategy )

The audit strategy that should be used when persisting audit data. The default stores only the revision, at which an entity was modified.

An alternative, the org.hibernate.envers.strategy.ValidityAuditStrategy stores both the start revision and the end revision. Together these define when an audit row was valid, hence the name ValidityAuditStrategy.

**org.hibernate.envers.audit_strategy_validity_end_rev_field_name** (default: REVEND)

The column name that will hold the end revision number in audit entities. This property is only valid if the validity audit strategy is used.

**org.hibernate.envers.audit_strategy_validity_store_revend_timestamp**(default: false )

Should the timestamp of the end revision be stored, until which the data was valid, in addition to the end revision itself. This is useful to be able to purge old Audit records out of a relational database by using table partitioning.

Partitioning requires a column that exists within the table. This property is only evaluated if the ValidityAuditStrategy is used.

**org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name**(default: REVEND_TSTMP )

Column name of the timestamp of the end revision until which the data was valid. Only used if the ValidityAuditStrategy is used, and org.hibernate.envers.audit_strategy_validity_store_revend_timestamp evaluates to true.

**org.hibernate.envers.use_revision_entity_with_native_id** (default: true )

Boolean flag that determines the strategy of revision number generation. Default implementation of revision entity uses native identifier generator.

If the current database engine does not support identity columns, users are advised to set this property to false.

In this case revision numbers are created by preconfigured org.hibernate.id.enhanced.SequenceStyleGenerator. See: org.hibernate.envers.DefaultRevisionEntity and org.hibernate.envers.enhanced.SequenceIdRevisionEntity.

**org.hibernate.envers.track_entities_changed_in_revision** (default: false )

Should entity types, that have been modified during each revision, be tracked. The default implementation creates REVCHANGES table that stores entity names of modified persistent objects. Single record encapsulates the revision identifier (foreign key to REVINFO table) and a string value. For more information, refer to Tracking entity names modified during revisions and Querying for entity types modified in a given revision.

**org.hibernate.envers.global_with_modified_flag** (default: false, can be individually overridden with @Audited( withModifiedFlag = true ) )

Should property modification flags be stored for all audited entities and all properties.

When set to true, for all properties an additional boolean column in the audit tables will be created, filled with information if the given property changed in the given revision.

When set to false, such column can be added to selected entities or properties using the @Audited annotation.

For more information, refer to Tracking entity changes at the property level and Querying for entity revisions that modified a given property.

**org.hibernate.envers.modified_flag_suffix** (default: _MOD )

The suffix for columns storing “Modified Flags”.

For example, a property called “age”, will by default get modified flag with column name “age_MOD”.

**org.hibernate.envers.modified_column_naming_strategy** (default: org.hibernate.envers.boot.internal.LegacyModifiedColumnNamingStrategy )

The naming strategy to be used for modified flag columns in the audit metadata.

**org.hibernate.envers.embeddable_set_ordinal_field_name** (default: SETORDINAL )

Name of column used for storing ordinal of the change in sets of embeddable elements.

**org.hibernate.envers.cascade_delete_revision** (default: false )

While deleting revision entry, remove data of associated audited entities. Requires database support for cascade row removal.

**org.hibernate.envers.allow_identifier_reuse** (default: false )

Guarantees proper validity audit strategy behavior when application reuses identifiers of deleted entities. Exactly one row with null end date exists for each identifier.

**org.hibernate.envers.original_id_prop_name** (default: originalId )

Specifies the composite-id key property name used by the audit table mappings.

**org.hibernate.envers.find_by_revision_exact_match** (default: false )

Specifies whether or not AuditReader#find methods which accept a revision-number argument are to find results based on fuzzy-match or exact-match behavior.

The old (legacy) behavior has always been to perform a fuzzy-match where these methods would return a match if any revision existed for the primary-key with a revision-number less-than or equal-to the revision method argument. This behavior is great when you want to find the snapshot of a non-related entity based on another entity’s revision number.

The new (optional) behavior when this option is enabled forces the query to perform an exact-match instead. In order for these methods to return a non-null value, a revision entry must exist for the entity with the specified primary key and revision number; otherwise the result will be null.

The following configuration options have been added recently and should be regarded as experimental:

  1. org.hibernate.envers.track_entities_changed_in_revision

  2. org.hibernate.envers.using_modified_flag

  3. org.hibernate.envers.modified_flag_suffix

  4. org.hibernate.envers.modified_column_naming_strategy

  5. org.hibernate.envers.original_id_prop_name

  6. org.hibernate.envers.find_by_revision_exact_match

21.3. Additional mapping annotations

The name of the audit table can be set on a per-entity basis, using the @AuditTable annotation. It may be tedious to add this annotation to every audited entity, so if possible, it’s better to use a prefix/suffix.

If you have a mapping with secondary tables, audit tables for them will be generated in the same way (by adding the prefix and suffix). If you wish to overwrite this behavior, you can use the @SecondaryAuditTable and @SecondaryAuditTables annotations.

If you’d like to override auditing behavior of some fields/properties inherited from @MappedSuperclass or in an embedded component, you can apply the @AuditOverride annotation on the subtype or usage site of the component.

If you want to audit a relation mapped with @OneToMany and @JoinColumn, please see Mapping exceptions for a description of the additional @AuditJoinTable annotation that you’ll probably want to use.

If you want to audit a relation, where the target entity is not audited (that is the case for example with dictionary-like entities, which don’t change and don’t have to be audited), just annotate it with @Audited( targetAuditMode = RelationTargetAuditMode.NOT_AUDITED ). Then, while reading historic versions of your entity, the relation will always point to the “current” related entity. By default Envers throws javax.persistence.EntityNotFoundException when “current” entity does not exist in the database. Apply @NotFound( action = NotFoundAction.IGNORE ) annotation to silence the exception and assign null value instead. The hereby solution causes implicit eager loading of to-one relations.

If you’d like to audit properties of a superclass of an entity, which are not explicitly audited (they don’t have the @Audited annotation on any properties or on the class), you can set the @AuditOverride( forClass = SomeEntity.class, isAudited = true/false ) annotation.

The @Audited annotation also features an auditParents attribute but it’s now deprecated in favor of @AuditOverride.

21.4. Choosing an audit strategy

After the basic configuration, it is important to choose the audit strategy that will be used to persist and retrieve audit information. There is a trade-off between the performance of persisting and the performance of querying the audit information. Currently, there are two audit strategies.

  1. The default audit strategy persists the audit data together with a start revision. For each row inserted, updated or deleted in an audited table, one or more rows are inserted in the audit tables, together with the start revision of its validity. Rows in the audit tables are never updated after insertion. Queries of audit information use subqueries to select the applicable rows in the audit tables.

    These subqueries are notoriously slow and difficult to index.
  2. The alternative is a validity audit strategy. This strategy stores the start-revision and the end-revision of audit information. For each row inserted, updated or deleted in an audited table, one or more rows are inserted in the audit tables, together with the start revision of its validity. But at the same time, the end-revision field of the previous audit rows (if available) is set to this revision. Queries on the audit information can then use ‘between start and end revision’ instead of subqueries as used by the default audit strategy.

    The consequence of this strategy is that persisting audit information will be a bit slower because of the extra updates involved, but retrieving audit information will be a lot faster.

    This can be improved even further by adding extra indexes.

21.4.1. Configuring the ValidityAuditStrategy

To better visualize how the ValidityAuditStrategy works, consider the following exercise where we replay the previous audit logging example for the Customer entity.

First, you need to configure the ValidityAuditStrategy:

Example 661. Configuring the ValidityAuditStrategy

  1. options.put(
  2. EnversSettings.AUDIT_STRATEGY,
  3. ValidityAuditStrategy.class.getName()
  4. );

If, you’re using the persistence.xml configuration file, then the mapping will look as follows:

  1. <property
  2. name="org.hibernate.envers.audit_strategy"
  3. value="org.hibernate.envers.strategy.ValidityAuditStrategy"
  4. />

Once you configured the ValidityAuditStrategy, the following schema is going to be automatically generated:

Example 662. Envers schema for the ValidityAuditStrategy

  1. create table Customer (
  2. id bigint not null,
  3. created_on timestamp,
  4. firstName varchar(255),
  5. lastName varchar(255),
  6. primary key (id)
  7. )
  8. create table Customer_AUD (
  9. id bigint not null,
  10. REV integer not null,
  11. REVTYPE tinyint,
  12. REVEND integer,
  13. created_on timestamp,
  14. firstName varchar(255),
  15. lastName varchar(255),
  16. primary key (id, REV)
  17. )
  18. create table REVINFO (
  19. REV integer generated by default as identity,
  20. REVTSTMP bigint,
  21. primary key (REV)
  22. )
  23. alter table Customer_AUD
  24. add constraint FK5ecvi1a0ykunrriib7j28vpdj
  25. foreign key (REV)
  26. references REVINFO
  27. alter table Customer_AUD
  28. add constraint FKqd4fy7ww1yy95wi4wtaonre3f
  29. foreign key (REVEND)
  30. references REVINFO

As you can see, the REVEND column is added as well as its foreign key to the REVINFO table.

When rerunning the previous Customer audit log queries against the ValidityAuditStrategy, we get the following results:

Example 663. Getting the first revision for the Customer entity

  1. select
  2. c.id as id1_1_,
  3. c.REV as REV2_1_,
  4. c.REVTYPE as REVTYPE3_1_,
  5. c.REVEND as REVEND4_1_,
  6. c.created_on as created_5_1_,
  7. c.firstName as firstNam6_1_,
  8. c.lastName as lastName7_1_
  9. from
  10. Customer_AUD c
  11. where
  12. c.REV <= ?
  13. and c.REVTYPE <> ?
  14. and (
  15. c.REVEND > ?
  16. or c.REVEND is null
  17. )
  18. -- binding parameter [1] as [INTEGER] - [1]
  19. -- binding parameter [2] as [INTEGER] - [2]
  20. -- binding parameter [3] as [INTEGER] - [1]

Compared to the default strategy, the ValidityAuditStrategy generates simpler queries that can render better SQL execution plans.

21.5. Revision Log

When Envers starts a new revision, it creates a new revision entity which stores information about the revision.

By default, that includes just:

revision number

An integral value (int/Integer or long/Long). Essentially, the primary key of the revision.

A revision number value should always be increasing and never overflows.

The default implementations provided by Envers use an int data type which has an upper bounds of Integer.MAX_VALUE. It is critical that users consider whether this upper bounds is feasible for your application. If a large range is needed, consider using a custom revision entity mapping using a long data type.

In the event that the revision number reaches its upper bounds wrapping around becoming negative, an AuditException will be thrown causing the current transaction to be rolled back. This guarantees that the audit history remains in a valid state that can be queried by the Envers Query API.

revision timestamp

Either a long/Long or java.util.Date value representing the instant at which the revision was made. When using a java.util.Date, instead of a long/Long for the revision timestamp, take care not to store it to a column data type which will lose precision.

Envers handles this information as an entity. By default it uses its own internal class to act as the entity, mapped to the REVINFO table. You can, however, supply your own approach to collecting this information which might be useful to capture additional details such as who made a change or the IP address from which the request came. There are two things you need to make this work:

  1. First, you will need to tell Envers about the entity you wish to use. Your entity must use the @org.hibernate.envers.RevisionEntity annotation. It must define the two attributes described above annotated with @org.hibernate.envers.RevisionNumber and @org.hibernate.envers.RevisionTimestamp, respectively. You can extend from org.hibernate.envers.DefaultRevisionEntity, if you wish, to inherit all these required behaviors.

    Simply add the custom revision entity as you do your normal entities and Envers will find it.

    It is an error for there to be multiple entities marked as @org.hibernate.envers.RevisionEntity.
  2. Second, you need to tell Envers how to create instances of your revision entity which is handled by the newRevision( Object revisionEntity ) method of the org.hibernate.envers.RevisionListener interface.

    You tell Envers your custom org.hibernate.envers.RevisionListener implementation to use by specifying it on the @org.hibernate.envers.RevisionEntity annotation, using the value attribute. If your RevisionListener class is inaccessible from @RevisionEntity (e.g. it exists in a different module), set org.hibernate.envers.revision_listener property to its fully qualified class name. Class name defined by the configuration parameter overrides the revision entity’s value attribute.

Considering we have a CurrentUser utility which stores the currently logged user:

Example 664. CurrentUser utility

  1. public static class CurrentUser {
  2. public static final CurrentUser INSTANCE = new CurrentUser();
  3. private static final ThreadLocal<String> storage = new ThreadLocal<>();
  4. public void logIn(String user) {
  5. storage.set( user );
  6. }
  7. public void logOut() {
  8. storage.remove();
  9. }
  10. public String get() {
  11. return storage.get();
  12. }
  13. }

Now, we need to provide a custom @RevisionEntity to store the currently logged user

Example 665. Custom @RevisionEntity example

  1. @Entity(name = "CustomRevisionEntity")
  2. @Table(name = "CUSTOM_REV_INFO")
  3. @RevisionEntity( CustomRevisionEntityListener.class )
  4. public static class CustomRevisionEntity extends DefaultRevisionEntity {
  5. private String username;
  6. public String getUsername() {
  7. return username;
  8. }
  9. public void setUsername( String username ) {
  10. this.username = username;
  11. }
  12. }

With the custom RevisionEntity implementation in place, we only need to provide the RevisionEntity implementation which acts as a factory of RevisionEntity instances.

Example 666. Custom @RevisionListener example

  1. public static class CustomRevisionEntityListener implements RevisionListener {
  2. public void newRevision( Object revisionEntity ) {
  3. CustomRevisionEntity customRevisionEntity =
  4. ( CustomRevisionEntity ) revisionEntity;
  5. customRevisionEntity.setUsername(
  6. CurrentUser.INSTANCE.get()
  7. );
  8. }
  9. }

When generating the database schema, Envers creates the following RevisionEntity table:

Example 667. Auto-generated RevisionEntity Envers table

  1. create table CUSTOM_REV_INFO (
  2. id integer not null,
  3. timestamp bigint not null,
  4. username varchar(255),
  5. primary key (id)
  6. )

You can see the username column in place.

Now, when inserting a Customer entity, Envers generates the following statements:

Example 668. Auditing using the custom @RevisionEntity instance

  1. CurrentUser.INSTANCE.logIn( "Vlad Mihalcea" );
  2. doInJPA( this::entityManagerFactory, entityManager -> {
  3. Customer customer = new Customer();
  4. customer.setId( 1L );
  5. customer.setFirstName( "John" );
  6. customer.setLastName( "Doe" );
  7. entityManager.persist( customer );
  8. } );
  9. CurrentUser.INSTANCE.logOut();
  1. insert
  2. into
  3. Customer
  4. (created_on, firstName, lastName, id)
  5. values
  6. (?, ?, ?, ?)
  7. -- binding parameter [1] as [TIMESTAMP] - [Thu Jul 27 15:45:00 EEST 2017]
  8. -- binding parameter [2] as [VARCHAR] - [John]
  9. -- binding parameter [3] as [VARCHAR] - [Doe]
  10. -- binding parameter [4] as [BIGINT] - [1]
  11. insert
  12. into
  13. CUSTOM_REV_INFO
  14. (timestamp, username, id)
  15. values
  16. (?, ?, ?)
  17. -- binding parameter [1] as [BIGINT] - [1501159500888]
  18. -- binding parameter [2] as [VARCHAR] - [Vlad Mihalcea]
  19. -- binding parameter [3] as [INTEGER] - [1]
  20. insert
  21. into
  22. Customer_AUD
  23. (REVTYPE, created_on, firstName, lastName, id, REV)
  24. values
  25. (?, ?, ?, ?, ?, ?)
  26. -- binding parameter [1] as [INTEGER] - [0]
  27. -- binding parameter [2] as [TIMESTAMP] - [Thu Jul 27 15:45:00 EEST 2017]
  28. -- binding parameter [3] as [VARCHAR] - [John]
  29. -- binding parameter [4] as [VARCHAR] - [Doe]
  30. -- binding parameter [5] as [BIGINT] - [1]
  31. -- binding parameter [6] as [INTEGER] - [1]

As demonstrated by the example above, the username is properly set and propagated to the CUSTOM_REV_INFO table.

This strategy is deprecated since version 5.2. The alternative is to use dependency injection offered as of version 5.3.

An alternative method to using the org.hibernate.envers.RevisionListener is to instead call the getCurrentRevision( Class<T> revisionEntityClass, boolean persist ) method of the org.hibernate.envers.AuditReader interface to obtain the current revision, and fill it with desired information.

The method accepts a persist parameter indicating whether the revision entity should be persisted prior to returning from this method:

    true

    ensures that the returned entity has access to its identifier value (revision number), but the revision entity will be persisted regardless of whether there are any audited entities changed.

    false

    means that the revision number will be null, but the revision entity will be persisted only if some audited entities have changed.

As of Hibernate Envers 5.3, dependency injection is now supported for a RevisionListener.

This feature is up to the various dependency frameworks, such as CDI and Spring, to supply the necessary implementation during Hibernate ORM bootstrap to support injection. If no qualifying implementation is supplied, the RevisionListener will be constructed without injection.

21.6. Tracking entity names modified during revisions

By default, entity types that have been changed in each revision are not being tracked. This implies the necessity to query all tables storing audited data in order to retrieve changes made during the specified revision. Envers provides a simple mechanism that creates REVCHANGES table which stores entity names of modified persistent objects. Single record encapsulates the revision identifier (foreign key to REVINFO table) and a string value.

Tracking of modified entity names can be enabled in three different ways:

  1. Set org.hibernate.envers.track_entities_changed_in_revision parameter to true. In this case org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity will be implicitly used as the revision log entity.

  2. Create a custom revision entity that extends org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity class.

    1. @Entity(name = "CustomTrackingRevisionEntity")
    2. @Table(name = "TRACKING_REV_INFO")
    3. @RevisionEntity
    4. public static class CustomTrackingRevisionEntity
    5. extends DefaultTrackingModifiedEntitiesRevisionEntity {
    6. }
  3. Mark an appropriate field of a custom revision entity with @org.hibernate.envers.ModifiedEntityNames annotation. The property is required to be of Set<String> type.

    1. @Entity(name = "CustomTrackingRevisionEntity")
    2. @Table(name = "TRACKING_REV_INFO")
    3. @RevisionEntity
    4. public static class CustomTrackingRevisionEntity extends DefaultRevisionEntity {
    5. @ElementCollection
    6. @JoinTable(
    7. name = "REVCHANGES",
    8. joinColumns = @JoinColumn( name = "REV" )
    9. )
    10. @Column( name = "ENTITYNAME" )
    11. @ModifiedEntityNames
    12. private Set<String> modifiedEntityNames = new HashSet<>();
    13. public Set<String> getModifiedEntityNames() {
    14. return modifiedEntityNames;
    15. }
    16. }

Considering we have a Customer entity illustrated by the following example:

Example 669. Customer entity before renaming

  1. @Audited
  2. @Entity(name = "Customer")
  3. public static class Customer {
  4. @Id
  5. private Long id;
  6. private String firstName;
  7. private String lastName;
  8. @Temporal( TemporalType.TIMESTAMP )
  9. @Column(name = "created_on")
  10. @CreationTimestamp
  11. private Date createdOn;
  12. //Getters and setters are omitted for brevity
  13. }

If the Customer entity class name is changed to ApplicationCustomer, Envers is going to insert a new record in the REVCHANGES table with the previous entity class name:

Example 670. Customer entity after renaming

  1. @Audited
  2. @Entity(name = "Customer")
  3. public static class ApplicationCustomer {
  4. @Id
  5. private Long id;
  6. private String firstName;
  7. private String lastName;
  8. @Temporal( TemporalType.TIMESTAMP )
  9. @Column(name = "created_on")
  10. @CreationTimestamp
  11. private Date createdOn;
  12. //Getters and setters are omitted for brevity
  13. }
  1. insert
  2. into
  3. REVCHANGES
  4. (REV, ENTITYNAME)
  5. values
  6. (?, ?)
  7. -- binding parameter [1] as [INTEGER] - [1]
  8. -- binding parameter [2] as [VARCHAR] - [org.hibernate.userguide.envers.EntityTypeChangeAuditTest$Customer]

Users, that have chosen one of the approaches listed above, can retrieve all entities modified in a specified revision by utilizing API described in Querying for entity types modified in a given revision.

Users are also allowed to implement custom mechanisms of tracking modified entity types. In this case, they shall pass their own implementation of org.hibernate.envers.EntityTrackingRevisionListener interface as the value of @org.hibernate.envers.RevisionEntity annotation.

EntityTrackingRevisionListener interface exposes one method that notifies whenever audited entity instance has been added, modified or removed within current revision boundaries.

Example 671. The EntityTrackingRevisionListener implementation

  1. public static class CustomTrackingRevisionListener implements EntityTrackingRevisionListener {
  2. @Override
  3. public void entityChanged(Class entityClass,
  4. String entityName,
  5. Serializable entityId,
  6. RevisionType revisionType,
  7. Object revisionEntity ) {
  8. String type = entityClass.getName();
  9. ( (CustomTrackingRevisionEntity) revisionEntity ).addModifiedEntityType( type );
  10. }
  11. @Override
  12. public void newRevision( Object revisionEntity ) {
  13. }
  14. }

The CustomTrackingRevisionListener adds the fully-qualified class name to the modifiedEntityTypes attribute of the CustomTrackingRevisionEntity.

Example 672. The RevisionEntity using the custom EntityTrackingRevisionListener

  1. @Entity(name = "CustomTrackingRevisionEntity")
  2. @Table(name = "TRACKING_REV_INFO")
  3. @RevisionEntity( CustomTrackingRevisionListener.class )
  4. public static class CustomTrackingRevisionEntity {
  5. @Id
  6. @GeneratedValue
  7. @RevisionNumber
  8. private int customId;
  9. @RevisionTimestamp
  10. private long customTimestamp;
  11. @OneToMany(
  12. mappedBy="revision",
  13. cascade={
  14. CascadeType.PERSIST,
  15. CascadeType.REMOVE
  16. }
  17. )
  18. private Set<EntityType> modifiedEntityTypes = new HashSet<>();
  19. public Set<EntityType> getModifiedEntityTypes() {
  20. return modifiedEntityTypes;
  21. }
  22. public void addModifiedEntityType(String entityClassName ) {
  23. modifiedEntityTypes.add( new EntityType( this, entityClassName ) );
  24. }
  25. }

The CustomTrackingRevisionEntity contains a @OneToMany list of ModifiedTypeRevisionEntity

Example 673. The EntityType encapsulates the entity type name before a class name modification

  1. @Entity(name = "EntityType")
  2. public static class EntityType {
  3. @Id
  4. @GeneratedValue
  5. private Integer id;
  6. @ManyToOne
  7. private CustomTrackingRevisionEntity revision;
  8. private String entityClassName;
  9. private EntityType() {
  10. }
  11. public EntityType(CustomTrackingRevisionEntity revision, String entityClassName) {
  12. this.revision = revision;
  13. this.entityClassName = entityClassName;
  14. }
  15. //Getters and setters are omitted for brevity
  16. }

Now, when fetching the CustomTrackingRevisionEntity, you can get access to the previous entity class name.

Example 674. Getting the EntityType through the CustomTrackingRevisionEntity

  1. AuditReader auditReader = AuditReaderFactory.get( entityManager );
  2. List<Number> revisions = auditReader.getRevisions(
  3. ApplicationCustomer.class,
  4. 1L
  5. );
  6. CustomTrackingRevisionEntity revEntity = auditReader.findRevision(
  7. CustomTrackingRevisionEntity.class,
  8. revisions.get( 0 )
  9. );
  10. Set<EntityType> modifiedEntityTypes = revEntity.getModifiedEntityTypes();
  11. assertEquals( 1, modifiedEntityTypes.size() );
  12. EntityType entityType = modifiedEntityTypes.iterator().next();
  13. assertEquals(
  14. Customer.class.getName(),
  15. entityType.getEntityClassName()
  16. );

21.7. Tracking entity changes at the property level

By default, the only information stored by Envers are revisions of modified entities. This approach lets users create audit queries based on historical values of entity properties. Sometimes it is useful to store additional metadata for each revision, when you are interested also in the type of changes, not only about the resulting values.

The feature described in Tracking entity names modified during revisions makes it possible to tell which entities were modified in a given revision.

The feature described here takes it one step further. Modification Flags enable Envers to track which properties of audited entities were modified in a given revision.

Tracking entity changes at the property level can be enabled by:

  1. setting org.hibernate.envers.global_with_modified_flag configuration property to true. This global switch will cause adding modification flags to be stored for all audited properties of all audited entities.

  2. using @Audited( withModifiedFlag = true ) on a property or on an entity.

The trade-off coming with this functionality is an increased size of audit tables and a very little, almost negligible, performance drop during audit writes. This is due to the fact that every tracked property has to have an accompanying boolean column in the schema that stores information about the property modifications. Of course, it is Enver’s job to fill these columns accordingly - no additional work by the developer is required. Because of costs mentioned, it is recommended to enable the feature selectively, when needed with use of the granular configuration means described above.

Example 675. Mapping for tracking entity changes at the property level

  1. @Audited(withModifiedFlag = true)
  2. @Entity(name = "Customer")
  3. public static class Customer {
  4. @Id
  5. private Long id;
  6. private String firstName;
  7. private String lastName;
  8. @Temporal( TemporalType.TIMESTAMP )
  9. @Column(name = "created_on")
  10. @CreationTimestamp
  11. private Date createdOn;
  12. //Getters and setters are omitted for brevity
  13. }
  1. create table Customer_AUD (
  2. id bigint not null,
  3. REV integer not null,
  4. REVTYPE tinyint,
  5. created_on timestamp,
  6. createdOn_MOD boolean,
  7. firstName varchar(255),
  8. firstName_MOD boolean,
  9. lastName varchar(255),
  10. lastName_MOD boolean,
  11. primary key (id, REV)
  12. )

As you can see, every property features a _MOD column (e.g. createdOn_MOD) in the audit log.

Example 676. Tracking entity changes at the property level example

  1. Customer customer = entityManager.find( Customer.class, 1L );
  2. customer.setLastName( "Doe Jr." );
  1. update
  2. Customer
  3. set
  4. created_on = ?,
  5. firstName = ?,
  6. lastName = ?
  7. where
  8. id = ?
  9. -- binding parameter [1] as [TIMESTAMP] - [2017-07-31 15:58:20.342]
  10. -- binding parameter [2] as [VARCHAR] - [John]
  11. -- binding parameter [3] as [VARCHAR] - [Doe Jr.]
  12. -- binding parameter [4] as [BIGINT] - [1]
  13. insert
  14. into
  15. REVINFO
  16. (REV, REVTSTMP)
  17. values
  18. (null, ?)
  19. -- binding parameter [1] as [BIGINT] - [1501505900439]
  20. insert
  21. into
  22. Customer_AUD
  23. (REVTYPE, created_on, createdOn_MOD, firstName, firstName_MOD, lastName, lastName_MOD, id, REV)
  24. values
  25. (?, ?, ?, ?, ?, ?, ?, ?, ?)
  26. -- binding parameter [1] as [INTEGER] - [1]
  27. -- binding parameter [2] as [TIMESTAMP] - [2017-07-31 15:58:20.342]
  28. -- binding parameter [3] as [BOOLEAN] - [false]
  29. -- binding parameter [4] as [VARCHAR] - [John]
  30. -- binding parameter [5] as [BOOLEAN] - [false]
  31. -- binding parameter [6] as [VARCHAR] - [Doe Jr.]
  32. -- binding parameter [7] as [BOOLEAN] - [true]
  33. -- binding parameter [8] as [BIGINT] - [1]
  34. -- binding parameter [9] as [INTEGER] - [2]

To see how “Modified Flags” can be utilized, check out the very simple query API that uses them: Querying for entity revisions that modified a given property.

21.8. Selecting strategy for tracking property level changes

By default, Envers uses the legacy modified column naming strategy. This strategy is designed to add columns based on the following rule-set:

  1. If property is annotated with @Audited and the modifiedColumnName attribute is specified, the column will directly be based on the supplied name.

  2. If property is not annotated with @Audited or if no modifiedColumnName attribute is given, the column will be named after the java class property, appended with the configured suffix, the default being _MOD.

While this strategy has no performance drawbacks, it does present concerns for users who prefer consistency without verbosity. Lets take the following entity mapping as an example.

  1. @Audited(withModifiedFlags = true)
  2. @Entity
  3. public class Customer {
  4. @Id
  5. private Integer id;
  6. @Column(name = "customer_name")
  7. private String name;
  8. }

This mapping will actually lead to some inconsistent naming between columns, see below for how the model’s name will be stored in customer_name but the modified column that tracks whether this column changes between revisions is named name_MOD.

  1. CREATE TABLE Customer_AUD (
  2. id bigint not null,
  3. REV integer not null,
  4. REVTYPE tinyint not null,
  5. customer_name varchar(255),
  6. name_MOD boolean,
  7. primary key(id, REV)
  8. )

An additional strategy called improved, aims to address these inconsistent column naming concerns. This strategy uses the following rule-set:

  1. Property is a Basic type (Single Column valued property)

    1. Use the modifiedColumnName directly if one is supplied on the property mapping

    2. Otherwise use the resolved ORM column name appended with the modified flag suffix configured value

  1. Property is an Association (to-one mapping) with a Foreign Key using a single column

    1. Use the modifiedColumnName directly if one is supplied on the property mapping

    2. Otherwise use the resolved ORM column name appended with the modified flag suffix configured value

  1. Property is an Association (to-one mapping) with a Foreign Key using multiple columns

    1. Use the modifiedColumnName directly if one is supplied on the property mapping

    2. Otherwise use the property name appended with the modified flag suffix configured value

  1. Property is an Embeddable

    1. Use the modifiedColumnName directly if one is supplied on the property mapping

    2. Otherwise use the property name appended with the modified flag suffix configured value

While using this strategy, the same Customer mapping will generate the following table schema:

  1. CREATE TABLE Customer_AUD (
  2. id bigint not null,
  3. REV integer not null,
  4. REVTYPE tinyint not null,
  5. customer_name varchar(255),
  6. customer_name_MOD boolean,
  7. primary key(id, REV)
  8. )

When already using Envers in conjunction with the modified columns flag feature, it is advised not to enable the new strategy immediately as schema changes would be required. You will need to either migrate your existing schema manually to adhere to the rules above or use the explicit modifiedColumnName attribute on the @Audited annotation for existing columns that use the feature.

To configure a custom strategy implementation or use the improved strategy, the configuration option org.hibernate.envers.modified_column_naming_strategy will need to be set. This option can be the fully qualified class name of a ModifiedColumnNameStrategy implementation or legacy or improved for either of the two provided implementations.

21.9. Queries

You can think of historic data as having two dimensions:

horizontal

The state of the database at a given revision. Thus, you can query for entities as they were at revision N.

vertical

The revisions, at which entities changed. Hence, you can query for revisions, in which a given entity changed.

The queries in Envers are similar to Hibernate Criteria queries, so if you are familiar with them, using Envers queries will be much easier.

The main limitation of the current queries implementation is that you cannot traverse relations. You can only specify constraints on the ids of the related entities, and only on the “owning” side of the relation. This, however, will be changed in future releases.

The queries on the audited data will be in many cases much slower than corresponding queries on “live” data, as, especially for the default audit strategy, they involve correlated subselects.

Queries are improved both in terms of speed and possibilities when using the validity audit strategy, which stores both start and end revisions for entities. See Configuring the ValidityAuditStrategy for a more detailed discussion.

21.10. Querying for entities of a class at a given revision

The entry point for this type of queries is:

Example 677. Getting the Customer entity at a given revision

  1. Customer customer = (Customer) AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, revisions.get( 0 ) )
  5. .getSingleResult();
  6. assertEquals("Doe", customer.getLastName());

21.11. Querying for entities using filtering criteria

You can then specify constraints, which should be met by the entities returned, by adding restrictions, which can be obtained using the AuditEntity factory class.

For example, to select only entities where the firstName property is equal to “John”:

Example 678. Getting the Customer audit log with a given firstName attribute value

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, true, true )
  5. .add( AuditEntity.property( "firstName" ).eq( "John" ) )
  6. .getResultList();
  7. assertEquals(2, customers.size());
  8. assertEquals( "Doe", customers.get( 0 ).getLastName() );
  9. assertEquals( "Doe Jr.", customers.get( 1 ).getLastName() );

And, to select only entities whose relationships are related to a given entity, you can use either the target entity or its identifier.

Example 679. Getting the Customer entities whose address attribute matches the given entity reference

  1. Address address = entityManager.getReference( Address.class, 1L );
  2. List<Customer> customers = AuditReaderFactory
  3. .get( entityManager )
  4. .createQuery()
  5. .forRevisionsOfEntity( Customer.class, true, true )
  6. .add( AuditEntity.property( "address" ).eq( address ) )
  7. .getResultList();
  8. assertEquals(2, customers.size());
  1. select
  2. c.id as id1_3_,
  3. c.REV as REV2_3_,
  4. c.REVTYPE as REVTYPE3_3_,
  5. c.REVEND as REVEND4_3_,
  6. c.created_on as created_5_3_,
  7. c.firstName as firstNam6_3_,
  8. c.lastName as lastName7_3_,
  9. c.address_id as address_8_3_
  10. from
  11. Customer_AUD c
  12. where
  13. c.address_id = ?
  14. order by
  15. c.REV asc
  16. -- binding parameter [1] as [BIGINT] - [1]

The same SQL is generated even if we provide the identifier instead of the target entity reference.

Example 680. Getting the Customer entities whose address identifier matches the given entity identifier

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, true, true )
  5. .add( AuditEntity.relatedId( "address" ).eq( 1L ) )
  6. .getResultList();
  7. assertEquals(2, customers.size());

Apart from strict equality matching, you can also use an IN clause to provide multiple entity identifiers:

Example 681. Getting the Customer entities whose address identifier matches one of the given entity identifiers

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, true, true )
  5. .add( AuditEntity.relatedId( "address" ).in( new Object[] { 1L, 2L } ) )
  6. .getResultList();
  7. assertEquals(2, customers.size());
  1. select
  2. c.id as id1_3_,
  3. c.REV as REV2_3_,
  4. c.REVTYPE as REVTYPE3_3_,
  5. c.REVEND as REVEND4_3_,
  6. c.created_on as created_5_3_,
  7. c.firstName as firstNam6_3_,
  8. c.lastName as lastName7_3_,
  9. c.address_id as address_8_3_
  10. from
  11. Customer_AUD c
  12. where
  13. c.address_id in (
  14. ? , ?
  15. )
  16. order by
  17. c.REV asc
  18. -- binding parameter [1] as [BIGINT] - [1]
  19. -- binding parameter [2] as [BIGINT] - [2]

You can limit the number of results, order them, and set aggregations and projections (except grouping) in the usual way. When your query is complete, you can obtain the results by calling the getSingleResult() or getResultList() methods.

A full query, can look for example like this:

Example 682. Getting the Customer entities using filtering and pagination

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, true, true )
  5. .addOrder( AuditEntity.property( "lastName" ).desc() )
  6. .add( AuditEntity.relatedId( "address" ).eq( 1L ) )
  7. .setFirstResult( 1 )
  8. .setMaxResults( 2 )
  9. .getResultList();
  10. assertEquals(1, customers.size());
  1. select
  2. c.id as id1_3_,
  3. c.REV as REV2_3_,
  4. c.REVTYPE as REVTYPE3_3_,
  5. c.REVEND as REVEND4_3_,
  6. c.created_on as created_5_3_,
  7. c.firstName as firstNam6_3_,
  8. c.lastName as lastName7_3_,
  9. c.address_id as address_8_3_
  10. from
  11. Customer_AUD c
  12. where
  13. c.address_id = ?
  14. order by
  15. c.lastName desc
  16. limit ?
  17. offset ?

21.12. Querying for revisions, at which entities of a given class changed

The entry point for this type of queries is:

  1. AuditQuery query = AuditReaderFactory.get( entityManager )
  2. .createQuery()
  3. .forRevisionsOfEntity( Customer.class, false, true );

You can add constraints to this query in the same way as to the previous one.

There are some additional possibilities:

  1. using AuditEntity.revisionNumber() you can specify constraints, projections and order on the revision number, in which the audited entity was modified.

  2. similarly, using AuditEntity.revisionProperty( propertyName ) you can specify constraints, projections and order on a property of the revision entity, corresponding to the revision in which the audited entity was modified.

  3. AuditEntity.revisionType() gives you access as above to the type of the revision (ADD, MOD, DEL).

Using these methods, you can order the query results by revision number, set projection or constraint the revision number to be greater or less than a specified value, etc. For example, the following query will select the smallest revision number, at which entity of class MyEntity with id entityId has changed, after revision number 2:

  1. Number revision = (Number) AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, false, true )
  5. .addProjection( AuditEntity.revisionNumber().min() )
  6. .add( AuditEntity.id().eq( 1L ) )
  7. .add( AuditEntity.revisionNumber().gt( 2 ) )
  8. .getSingleResult();

The second additional feature you can use in queries for revisions is the ability to maximize/minimize a property.

For example, if you want to select the smallest possible revision at which the value of the createdOn attribute was larger then a given value, you can run the following query:

  1. Number revision = (Number) AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, false, true )
  5. .addProjection( AuditEntity.revisionNumber().min() )
  6. .add( AuditEntity.id().eq( 1L ) )
  7. .add(
  8. AuditEntity.property( "createdOn" )
  9. .minimize()
  10. .add( AuditEntity.property( "createdOn" )
  11. .ge(
  12. Timestamp.from(
  13. LocalDateTime.now()
  14. .minusDays( 1 )
  15. .toInstant( ZoneOffset.UTC )
  16. )
  17. )
  18. )
  19. )
  20. .getSingleResult();

The minimize() and maximize() methods return a criterion, to which you can add constraints, which must be met by the entities with the maximized/minimized properties.

You probably also noticed that there are two boolean parameters, passed when creating the query.

selectEntitiesOnly

The first parameter is only valid when you don’t set an explicit projection.

If true, the result of the query will be a list of entities (which changed at revisions satisfying the specified constraints).

If false, the result will be a list of three element arrays:

  • the first element will be the changed entity instance.

  • the second will be an entity containing revision data (if no custom entity is used, this will be an instance of DefaultRevisionEntity).

  • the third will be the type of the revision (one of the values of the RevisionType enumeration: ADD, MOD, DEL).

selectDeletedEntities

The second parameter specifies if revisions, in which the entity was deleted should be included in the results.

If yes, such entities will have the revision type DEL and all attributes, except the id, will be set to null.

Another useful feature is AggregatedAuditExpression#computeAggregationInInstanceContext(). This can be used to create an aggregate query based on the entity instance primary key.

For example, if you wanted to locate all customers but only wanted to retrieve the instances with the maximum revision number, you would use the following query:

  1. List<Customer> results = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, true, false )
  5. .add( AuditEntity.revisionNumber().maximize().computeAggregationInInstanceContext() )
  6. .getResultList();

In other words, the result set would contain a list of Customer instances, one per primary key. Each instance would hold the audited property data at the maximum revision number for each Customer primary key.

21.13. Querying for entity revisions that modified a given property

For the two types of queries described above it’s possible to use special Audit criteria called hasChanged() and hasNotChanged() that make use of the functionality described in Tracking entity changes at the property level.

Let’s have a look at various queries that can benefit from these two criteria.

First, you must make sure that your entity can track modification flags:

Example 683. Valid only when audit logging tracks entity attribute modification flags

  1. @Audited( withModifiedFlag = true )

The following query will return all revisions of the Customer entity with the given id, for which the lastName property has changed.

Example 684. Getting all Customer revisions for which the lastName attribute has changed

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, false, true )
  5. .add( AuditEntity.id().eq( 1L ) )
  6. .add( AuditEntity.property( "lastName" ).hasChanged() )
  7. .getResultList();
  1. select
  2. c.id as id1_3_0_,
  3. c.REV as REV2_3_0_,
  4. defaultrev1_.REV as REV1_4_1_,
  5. c.REVTYPE as REVTYPE3_3_0_,
  6. c.REVEND as REVEND4_3_0_,
  7. c.created_on as created_5_3_0_,
  8. c.createdOn_MOD as createdO6_3_0_,
  9. c.firstName as firstNam7_3_0_,
  10. c.firstName_MOD as firstNam8_3_0_,
  11. c.lastName as lastName9_3_0_,
  12. c.lastName_MOD as lastNam10_3_0_,
  13. c.address_id as address11_3_0_,
  14. c.address_MOD as address12_3_0_,
  15. defaultrev1_.REVTSTMP as REVTSTMP2_4_1_
  16. from
  17. Customer_AUD c cross
  18. join
  19. REVINFO defaultrev1_
  20. where
  21. c.id = ?
  22. and c.lastName_MOD = ?
  23. and c.REV=defaultrev1_.REV
  24. order by
  25. c.REV asc
  26. -- binding parameter [1] as [BIGINT] - [1]
  27. -- binding parameter [2] as [BOOLEAN] - [true]

Using this query we won’t get all other revisions in which lastName wasn’t touched. From the SQL query you can see that the lastName_MOD column is being used in the WHERE clause, hence the aforementioned requirement for tracking modification flags.

Of course, nothing prevents users from combining hasChanged condition with some additional criteria.

Example 685. Getting all Customer revisions for which the lastName attribute has changed and the firstName attribute has not changed

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forRevisionsOfEntity( Customer.class, false, true )
  5. .add( AuditEntity.id().eq( 1L ) )
  6. .add( AuditEntity.property( "lastName" ).hasChanged() )
  7. .add( AuditEntity.property( "firstName" ).hasNotChanged() )
  8. .getResultList();
  1. select
  2. c.id as id1_3_0_,
  3. c.REV as REV2_3_0_,
  4. defaultrev1_.REV as REV1_4_1_,
  5. c.REVTYPE as REVTYPE3_3_0_,
  6. c.REVEND as REVEND4_3_0_,
  7. c.created_on as created_5_3_0_,
  8. c.createdOn_MOD as createdO6_3_0_,
  9. c.firstName as firstNam7_3_0_,
  10. c.firstName_MOD as firstNam8_3_0_,
  11. c.lastName as lastName9_3_0_,
  12. c.lastName_MOD as lastNam10_3_0_,
  13. c.address_id as address11_3_0_,
  14. c.address_MOD as address12_3_0_,
  15. defaultrev1_.REVTSTMP as REVTSTMP2_4_1_
  16. from
  17. Customer_AUD c cross
  18. join
  19. REVINFO defaultrev1_
  20. where
  21. c.id=?
  22. and c.lastName_MOD=?
  23. and c.firstName_MOD=?
  24. and c.REV=defaultrev1_.REV
  25. order by
  26. c.REV asc
  27. -- binding parameter [1] as [BIGINT] - [1]
  28. -- binding parameter [2] as [BOOLEAN] - [true]
  29. -- binding parameter [3] as [BOOLEAN] - [false]

To get the Customer entities changed at a given revisionNumber with lastName modified and firstName untouched, we have to use the forEntitiesModifiedAtRevision query:

Example 686. Getting the Customer entity for a given revision if the lastName attribute has changed and the firstName attribute has not changed

  1. Customer customer = (Customer) AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesModifiedAtRevision( Customer.class, 2 )
  5. .add( AuditEntity.id().eq( 1L ) )
  6. .add( AuditEntity.property( "lastName" ).hasChanged() )
  7. .add( AuditEntity.property( "firstName" ).hasNotChanged() )
  8. .getSingleResult();
  1. select
  2. c.id as id1_3_,
  3. c.REV as REV2_3_,
  4. c.REVTYPE as REVTYPE3_3_,
  5. c.REVEND as REVEND4_3_,
  6. c.created_on as created_5_3_,
  7. c.createdOn_MOD as createdO6_3_,
  8. c.firstName as firstNam7_3_,
  9. c.firstName_MOD as firstNam8_3_,
  10. c.lastName as lastName9_3_,
  11. c.lastName_MOD as lastNam10_3_,
  12. c.address_id as address11_3_,
  13. c.address_MOD as address12_3_
  14. from
  15. Customer_AUD c
  16. where
  17. c.REV=?
  18. and c.id=?
  19. and c.lastName_MOD=?
  20. and c.firstName_MOD=?
  21. -- binding parameter [1] as [INTEGER] - [2]
  22. -- binding parameter [2] as [BIGINT] - [1]
  23. -- binding parameter [3] as [BOOLEAN] - [true]
  24. -- binding parameter [4] as [BOOLEAN] - [false]

21.14. Querying for revisions of entity including property names that were modified

This feature described here is still considered experimental. It is subject to change in future releases based on user feedback to improve its usefulness.

Sometimes it may be useful to query entity revisions and also determine all the properties of that revision which were modified without having to issue multiple queries using hasChanged() and hasNotChanged() criteria.

You can now obtain this information easily by using the following query:

Example 687. Querying entity revisions including property names modified.

  1. List results = AuditReaderFactory.get( entityManager )
  2. .createQuery()
  3. .forRevisionsOfEntityWithChanges( Customer.class, false )
  4. .add( AuditEntity.id().eq( 1L ) )
  5. .getResultList();
  6. for ( Object entry : results ) {
  7. final Object[] array = (Object[]) entry;
  8. final Set<String> propertiesChanged = (Set<String>) array[3];
  9. for ( String propertyName : propertiesChanged ) {
  10. /* Do something useful with the modified property `propertyName` */
  11. }
  12. }

21.15. Querying for entity types modified in a given revision

The methods described below can be used only when the default mechanism of tracking changed entity types is enabled (see Tracking entity names modified during revisions).

This basic query allows retrieving entity names and corresponding Java classes changed in a specified revision:

Example 688. Retrieving entity names and corresponding Java classes changed in a specified revision

  1. assertEquals(
  2. "org.hibernate.userguide.envers.EntityTypeChangeAuditTest$Customer",
  3. AuditReaderFactory
  4. .get( entityManager )
  5. .getCrossTypeRevisionChangesReader()
  6. .findEntityTypes( 1 )
  7. .iterator().next()
  8. .getFirst()
  9. );
  1. assertEquals(
  2. "org.hibernate.userguide.envers.EntityTypeChangeAuditTest$ApplicationCustomer",
  3. AuditReaderFactory
  4. .get( entityManager )
  5. .getCrossTypeRevisionChangesReader()
  6. .findEntityTypes( 2 )
  7. .iterator().next()
  8. .getFirst()
  9. );

Other queries (also accessible from org.hibernate.envers.CrossTypeRevisionChangesReader):

List<Object> findEntities(Number)

Returns snapshots of all audited entities changed (added, updated and removed) in a given revision. Executes N + 1 SQL queries, where N is a number of different entity classes modified within specified revision.

List<Object> findEntities(Number, RevisionType)

Returns snapshots of all audited entities changed (added, updated or removed) in a given revision filtered by modification type. Executes N + 1 SQL queries, where N is a number of different entity classes modified within specified revision.

Map<RevisionType, List<Object>> findEntitiesGroupByRevisionType(Number)

Returns a map containing lists of entity snapshots grouped by modification operation (e.g. addition, update and removal). Executes 3N + 1 SQL queries, where N is a number of different entity classes modified within specified revision.

21.16. Querying for entities using entity relation joins

Relation join queries are considered experimental and may change in future releases.

Audit queries support the ability to apply constraints, projections, and sort operations based on entity relations. In order to traverse entity relations through an audit query, you must use the relation traversal API with a join type.

Relation joins can be applied to many-to-one and one-to-one mappings only when using JoinType.LEFT or JoinType.INNER.

The basis for creating an entity relation join query is as follows:

Example 689. INNER JOIN entity audit query

  1. AuditQuery innerJoinAuditQuery = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, 1 )
  5. .traverseRelation( "address", JoinType.INNER );

Example 690. LEFT JOIN entity audit query

  1. AuditQuery innerJoinAuditQuery = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, 1 )
  5. .traverseRelation( "address", JoinType.LEFT );

Like any other query, constraints may be added to restrict the results.

For example, to find all Customer entities at a given revision whose addresses are in România, you can use the following query:

Example 691. Filtering the join relation with a WHERE clause predicate

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, 1 )
  5. .traverseRelation( "address", JoinType.INNER )
  6. .add( AuditEntity.property( "country" ).eq( "România" ) )
  7. .getResultList();
  1. select
  2. c.id as id1_3_,
  3. c.REV as REV2_3_,
  4. c.REVTYPE as REVTYPE3_3_,
  5. c.REVEND as REVEND4_3_,
  6. c.created_on as created_5_3_,
  7. c.firstName as firstNam6_3_,
  8. c.lastName as lastName7_3_,
  9. c.address_id as address_8_3_
  10. from
  11. Customer_AUD c
  12. inner join
  13. Address_AUD a
  14. on (
  15. c.address_id=a.id
  16. or (
  17. c.address_id is null
  18. )
  19. and (
  20. a.id is null
  21. )
  22. )
  23. where
  24. c.REV<=?
  25. and c.REVTYPE<>?
  26. and (
  27. c.REVEND>?
  28. or c.REVEND is null
  29. )
  30. and a.REV<=?
  31. and a.country=?
  32. and (
  33. a.REVEND>?
  34. or a.REVEND is null
  35. )
  36. -- binding parameter [1] as [INTEGER] - [1]
  37. -- binding parameter [2] as [INTEGER] - [2]
  38. -- binding parameter [3] as [INTEGER] - [1]
  39. -- binding parameter [4] as [INTEGER] - [1]
  40. -- binding parameter [5] as [VARCHAR] - [România]
  41. -- binding parameter [6] as [INTEGER] - [1]

It is also possible to traverse beyond the first relation in an entity graph.

For example, to find all Customer entities at a given revision with the country attribute of the address property being România:

Example 692. Filtering a nested join relation with a WHERE clause predicate

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, 1 )
  5. .traverseRelation( "address", JoinType.INNER )
  6. .traverseRelation( "country", JoinType.INNER )
  7. .add( AuditEntity.property( "name" ).eq( "România" ) )
  8. .getResultList();
  9. assertEquals( 1, customers.size() );
  1. select
  2. cu.id as id1_5_,
  3. cu.REV as REV2_5_,
  4. cu.REVTYPE as REVTYPE3_5_,
  5. cu.REVEND as REVEND4_5_,
  6. cu.created_on as created_5_5_,
  7. cu.firstName as firstNam6_5_,
  8. cu.lastName as lastName7_5_,
  9. cu.address_id as address_8_5_
  10. from
  11. Customer_AUD cu
  12. inner join
  13. Address_AUD a
  14. on (
  15. cu.address_id=a.id
  16. or (
  17. cu.address_id is null
  18. )
  19. and (
  20. a.id is null
  21. )
  22. )
  23. inner join
  24. Country_AUD co
  25. on (
  26. a.country_id=co.id
  27. or (
  28. a.country_id is null
  29. )
  30. and (
  31. co.id is null
  32. )
  33. )
  34. where
  35. cu.REV<=?
  36. and cu.REVTYPE<>?
  37. and (
  38. cu.REVEND>?
  39. or cu.REVEND is null
  40. )
  41. and a.REV<=?
  42. and (
  43. a.REVEND>?
  44. or a.REVEND is null
  45. )
  46. and co.REV<=?
  47. and co.name=?
  48. and (
  49. co.REVEND>?
  50. or co.REVEND is null
  51. )
  52. -- binding parameter [1] as [INTEGER] - [1]
  53. -- binding parameter [2] as [INTEGER] - [2]
  54. -- binding parameter [3] as [INTEGER] - [1]
  55. -- binding parameter [4] as [INTEGER] - [1]
  56. -- binding parameter [5] as [INTEGER] - [1]
  57. -- binding parameter [6] as [INTEGER] - [1]
  58. -- binding parameter [7] as [VARCHAR] - [România]
  59. -- binding parameter [8] as [INTEGER] - [1]

Constraints may also be added to the properties of nested joined relations, such as testing for null.

For example, the following query illustrates how to find all Customer entities at a given revision having the address in Cluj-Napoca or the address does not have any country entity reference:

Example 693. Filtering a join relation using multiple predicates

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, 1 )
  5. .traverseRelation( "address", JoinType.LEFT, "a" )
  6. .add(
  7. AuditEntity.or(
  8. AuditEntity.property( "a", "city" ).eq( "Cluj-Napoca" ),
  9. AuditEntity.relatedId( "country" ).eq( null )
  10. )
  11. )
  12. .getResultList();
  1. select
  2. c.id as id1_5_,
  3. c.REV as REV2_5_,
  4. c.REVTYPE as REVTYPE3_5_,
  5. c.REVEND as REVEND4_5_,
  6. c.created_on as created_5_5_,
  7. c.firstName as firstNam6_5_,
  8. c.lastName as lastName7_5_,
  9. c.address_id as address_8_5_
  10. from
  11. Customer_AUD c
  12. left outer join
  13. Address_AUD a
  14. on (
  15. c.address_id=a.id
  16. or (
  17. c.address_id is null
  18. )
  19. and (
  20. a.id is null
  21. )
  22. )
  23. where
  24. c.REV<=?
  25. and c.REVTYPE<>?
  26. and (
  27. c.REVEND>?
  28. or c.REVEND is null
  29. )
  30. and (
  31. a.REV is null
  32. or a.REV<=?
  33. and (
  34. a.REVEND>?
  35. or a.REVEND is null
  36. )
  37. )
  38. and (
  39. a.city=?
  40. or a.country_id is null
  41. )
  42. -- binding parameter [1] as [INTEGER] - [1]
  43. -- binding parameter [2] as [INTEGER] - [2]
  44. -- binding parameter [3] as [INTEGER] - [1]
  45. -- binding parameter [4] as [INTEGER] - [1]
  46. -- binding parameter [5] as [INTEGER] - [1]
  47. -- binding parameter [6] as [VARCHAR] - [Cluj-Napoca]

Queries can use the up method to navigate back up the entity graph.

Disjunction criterion may also be applied to relation join queries.

For example, the following query will find all Customer entities at a given revision where the country name is România or that the Customer lives in Cluj-Napoca:

Example 694. Filtering a nested join relation using multiple predicates

  1. List<Customer> customers = AuditReaderFactory
  2. .get( entityManager )
  3. .createQuery()
  4. .forEntitiesAtRevision( Customer.class, 1 )
  5. .traverseRelation( "address", JoinType.INNER, "a" )
  6. .traverseRelation( "country", JoinType.INNER, "cn" )
  7. .up()
  8. .up()
  9. .add(
  10. AuditEntity.disjunction()
  11. .add( AuditEntity.property( "a", "city" ).eq( "Cluj-Napoca" ) )
  12. .add( AuditEntity.property( "cn", "name" ).eq( "România" ) )
  13. )
  14. .addOrder( AuditEntity.property( "createdOn" ).asc() )
  15. .getResultList();
  1. select
  2. cu.id as id1_5_,
  3. cu.REV as REV2_5_,
  4. cu.REVTYPE as REVTYPE3_5_,
  5. cu.REVEND as REVEND4_5_,
  6. cu.created_on as created_5_5_,
  7. cu.firstName as firstNam6_5_,
  8. cu.lastName as lastName7_5_,
  9. cu.address_id as address_8_5_
  10. from
  11. Customer_AUD cu
  12. inner join
  13. Address_AUD a
  14. on (
  15. cu.address_id=a.id
  16. or (
  17. cu.address_id is null
  18. )
  19. and (
  20. a.id is null
  21. )
  22. )
  23. inner join
  24. Country_AUD co
  25. on (
  26. a.country_id=co.id
  27. or (
  28. a.country_id is null
  29. )
  30. and (
  31. co.id is null
  32. )
  33. )
  34. where
  35. cu.REV<=?
  36. and cu.REVTYPE<>?
  37. and (
  38. cu.REVEND>?
  39. or cu.REVEND is null
  40. )
  41. and (
  42. a.city=?
  43. or co.name=?
  44. )
  45. and a.REV<=?
  46. and (
  47. a.REVEND>?
  48. or a.REVEND is null
  49. )
  50. and co.REV<=?
  51. and (
  52. co.REVEND>?
  53. or co.REVEND is null
  54. )
  55. order by
  56. cu.created_on asc
  57. -- binding parameter [1] as [INTEGER] - [1]
  58. -- binding parameter [2] as [INTEGER] - [2]
  59. -- binding parameter [3] as [INTEGER] - [1]
  60. -- binding parameter [4] as [VARCHAR] - [Cluj-Napoca]
  61. -- binding parameter [5] as [VARCHAR] - [România]
  62. -- binding parameter [6] as [INTEGER] - [1]
  63. -- binding parameter [7] as [INTEGER] - [1]
  64. -- binding parameter [8] as [INTEGER] - [1]
  65. -- binding parameter [9] as [INTEGER] - [1]

Lastly, this example illustrates how related entity properties can be compared in a single constraint.

Assuming the Customer and the Address were previously changed as follows:

Example 695. Changing the Address to match the Country name

  1. Customer customer = entityManager.createQuery(
  2. "select c " +
  3. "from Customer c " +
  4. "join fetch c.address a " +
  5. "join fetch a.country " +
  6. "where c.id = :id", Customer.class )
  7. .setParameter( "id", 1L )
  8. .getSingleResult();
  9. customer.setLastName( "Doe Sr." );
  10. customer.getAddress().setCity(
  11. customer.getAddress().getCountry().getName()
  12. );

The following query shows how to find the Customer entities where the city property of the address attribute equals the name of the associated country attribute.

Example 696. Filtering a nested join relation using multiple predicates

  1. List<Number> revisions = AuditReaderFactory.get( entityManager ).getRevisions(
  2. Customer.class,
  3. 1L
  4. );
  5. List<Customer> customers = AuditReaderFactory
  6. .get( entityManager )
  7. .createQuery()
  8. .forEntitiesAtRevision( Customer.class, revisions.get( revisions.size() - 1 ) )
  9. .traverseRelation( "address", JoinType.INNER, "a" )
  10. .traverseRelation( "country", JoinType.INNER, "cn" )
  11. .up()
  12. .up()
  13. .add( AuditEntity.property( "a", "city" ).eqProperty( "cn", "name" ) )
  14. .getResultList();
  1. select
  2. cu.id as id1_5_,
  3. cu.REV as REV2_5_,
  4. cu.REVTYPE as REVTYPE3_5_,
  5. cu.REVEND as REVEND4_5_,
  6. cu.created_on as created_5_5_,
  7. cu.firstName as firstNam6_5_,
  8. cu.lastName as lastName7_5_,
  9. cu.address_id as address_8_5_
  10. from
  11. Customer_AUD cu
  12. inner join
  13. Address_AUD a
  14. on (
  15. cu.address_id=a.id
  16. or (
  17. cu.address_id is null
  18. )
  19. and (
  20. a.id is null
  21. )
  22. )
  23. inner join
  24. Country_AUD cr
  25. on (
  26. a.country_id=cr.id
  27. or (
  28. a.country_id is null
  29. )
  30. and (
  31. cr.id is null
  32. )
  33. )
  34. where
  35. cu.REV<=?
  36. and cu.REVTYPE<>?
  37. and a.city=cr.name
  38. and (
  39. cu.REVEND>?
  40. or cu.REVEND is null
  41. )
  42. and a.REV<=?
  43. and (
  44. a.REVEND>?
  45. or a.REVEND is null
  46. )
  47. and cr.REV<=?
  48. and (
  49. cr.REVEND>?
  50. or cr.REVEND is null
  51. )
  52. -- binding parameter [1] as [INTEGER] - [2]
  53. -- binding parameter [2] as [INTEGER] - [2]
  54. -- binding parameter [3] as [INTEGER] - [2]
  55. -- binding parameter [4] as [INTEGER] - [2]
  56. -- binding parameter [5] as [INTEGER] - [2]
  57. -- binding parameter [6] as [INTEGER] - [2]
  58. -- binding parameter [7] as [INTEGER] - [2]

21.17. Querying for revision information without loading entities

Sometimes, it may be useful to load information about revisions to find out who performed specific revisions or to know what entity names were modified but the change log about the related audited entities isn’t needed. This API allows an efficient way to get the revision information entity log without instantiating the actual entities themselves.

Here is a simple example:

  1. AuditQuery query = getAuditReader().createQuery()
  2. .forRevisionsOfEntity( DefaultRevisionEntity.class, true )
  3. .add( AuditEntity.revisionNumber().between( 1, 25 ) );

This query will return all revision information entities for revisions between 1 and 25 including those which are related to deletions. If deletions are not of interest, you would pass false as the second argument.

Note that this query uses the DefaultRevisionEntity class type. The class provided will vary depending on the configuration properties used to configure Envers or if you supply your own revision entity. Typically users who will use this API will likely be providing a custom revision entity implementation to obtain custom information being maintained per revision.

21.18. Conditional auditing

Envers persists audit data in reaction to various Hibernate events (e.g. post update, post insert, and so on), using a series of event listeners from the org.hibernate.envers.event.spi package. By default, if the Envers jar is in the classpath, the event listeners are auto-registered with Hibernate.

Conditional auditing can be implemented by overriding some of the Envers event listeners. To use customized Envers event listeners, the following steps are needed:

  1. Turn off automatic Envers event listeners registration by setting the hibernate.envers.autoRegisterListeners Hibernate property to false.

  2. Create subclasses for appropriate event listeners. For example, if you want to conditionally audit entity insertions, extend the org.hibernate.envers.event.spi.EnversPostInsertEventListenerImpl class. Place the conditional-auditing logic in the subclasses, call the super method if auditing should be performed.

  3. Create your own implementation of org.hibernate.integrator.spi.Integrator, similar to org.hibernate.envers.boot.internal.EnversIntegrator. Use your event listener classes instead of the default ones.

  4. For the integrator to be automatically used when Hibernate starts up, you will need to add a META-INF/services/org.hibernate.integrator.spi.Integrator file to your jar. The file should contain the fully qualified name of the class implementing the interface.

The use of hibernate.listeners.envers.autoRegister has been deprecated. The new hibernate.envers.autoRegisterListeners configuration setting should be used instead.

21.19. Understanding the Envers Schema

For each audited entity (that is, for each entity containing at least one audited field), an audit table is created. By default, the audit table’s name is created by adding an “_AUD” suffix to the original table name, but this can be overridden by specifying a different suffix/prefix in the configuration properties or per-entity using the @org.hibernate.envers.AuditTable annotation.

The audit table contains the following columns:

id

id of the original entity (this can be more then one column in the case of composite primary keys).

revision number

an integer, which matches to the revision number in the revision entity table.

revision type

The org.hibernate.envers.RevisionType enumeration ordinal stating if the change represents an INSERT, UPDATE or DELETE.

audited fields

properties from the original entity being audited.

The primary key of the audit table is the combination of the original id of the entity and the revision number, so there can be at most one historic entry for a given entity instance at a given revision.

The current entity data is stored in the original table and in the audit table. This is a duplication of data, however, as this solution makes the query system much more powerful, and as memory is cheap, hopefully, this won’t be a major drawback for the users.

A row in the audit table with entity id ID, revision N, and data D means: entity with id ID has data D from revision N upwards. Hence, if we want to find an entity at revision M, we have to search for a row in the audit table, which has the revision number smaller or equal to M, but as large as possible. If no such row is found, or a row with a “deleted” marker is found, it means that the entity didn’t exist at that revision.

The “revision type” field can currently have three values: 0, 1 and 2, which means ADD, MOD, and DEL, respectively. A row with a revision of type DEL will only contain the id of the entity and no data (all fields NULL), as it only serves as a marker saying “this entity was deleted at that revision”.

Additionally, there is a revision entity table which contains the information about the global revision. By default, the generated table is named REVINFO and contains just two columns: ID and TIMESTAMP. A row is inserted into this table on each new revision, that is, on each commit of a transaction, which changes audited data. The name of this table can be configured, the name of its columns as well as adding additional columns can be achieved as discussed in Revision Log.

While global revisions are a good way to provide correct auditing of relations, some people have pointed out that this may be a bottleneck in systems where data is very often modified.

One viable solution is to introduce an option to have an entity “locally revisioned”, that is revisions would be created for it independently. This would not enable correct versioning of relations, but it would work without the REVINFO table.

Another possibility is to introduce a notion of “revisioning groups”, which would group entities sharing the same revision numbering. Each such group would have to consist of one or more strongly connected components belonging to the entity graph induced by relations between entities.

Your opinions on the subject are very welcome on the forum.

21.20. Generating Envers schema with Hibernate hbm2ddl tool

If you would like to generate the database schema file with Hibernate, you simply need to use the hbm2ddl too.

This task will generate the definitions of all entities, both of those which are audited by Envers and those which are not.

See the Schema generation chapter for more info.

For the following entities, Hibernate is going to generate the following database schema:

Example 697. Filtering a nested join relation using multiple predicates

  1. @Audited
  2. @Entity(name = "Customer")
  3. public static class Customer {
  4. @Id
  5. private Long id;
  6. private String firstName;
  7. private String lastName;
  8. @Temporal( TemporalType.TIMESTAMP )
  9. @Column(name = "created_on")
  10. @CreationTimestamp
  11. private Date createdOn;
  12. @ManyToOne(fetch = FetchType.LAZY)
  13. private Address address;
  14. //Getters and setters omitted for brevity
  15. }
  16. @Audited
  17. @Entity(name = "Address")
  18. public static class Address {
  19. @Id
  20. private Long id;
  21. @ManyToOne(fetch = FetchType.LAZY)
  22. private Country country;
  23. private String city;
  24. private String street;
  25. private String streetNumber;
  26. //Getters and setters omitted for brevity
  27. }
  28. @Audited
  29. @Entity(name = "Country")
  30. public static class Country {
  31. @Id
  32. private Long id;
  33. private String name;
  34. //Getters and setters omitted for brevity
  35. }
  1. create table Address (
  2. id bigint not null,
  3. city varchar(255),
  4. street varchar(255),
  5. streetNumber varchar(255),
  6. country_id bigint,
  7. primary key (id)
  8. )
  9. create table Address_AUD (
  10. id bigint not null,
  11. REV integer not null,
  12. REVTYPE tinyint,
  13. REVEND integer,
  14. city varchar(255),
  15. street varchar(255),
  16. streetNumber varchar(255),
  17. country_id bigint,
  18. primary key (id, REV)
  19. )
  20. create table Country (
  21. id bigint not null,
  22. name varchar(255),
  23. primary key (id)
  24. )
  25. create table Country_AUD (
  26. id bigint not null,
  27. REV integer not null,
  28. REVTYPE tinyint,
  29. REVEND integer,
  30. name varchar(255),
  31. primary key (id, REV)
  32. )
  33. create table Customer (
  34. id bigint not null,
  35. created_on timestamp,
  36. firstName varchar(255),
  37. lastName varchar(255),
  38. address_id bigint,
  39. primary key (id)
  40. )
  41. create table Customer_AUD (
  42. id bigint not null,
  43. REV integer not null,
  44. REVTYPE tinyint,
  45. REVEND integer,
  46. created_on timestamp,
  47. firstName varchar(255),
  48. lastName varchar(255),
  49. address_id bigint,
  50. primary key (id, REV)
  51. )
  52. create table REVINFO (
  53. REV integer generated by default as identity,
  54. REVTSTMP bigint,
  55. primary key (REV)
  56. )
  57. alter table Address
  58. add constraint FKpr4rl83u5fv832kdihl6w3kii
  59. foreign key (country_id)
  60. references Country
  61. alter table Address_AUD
  62. add constraint FKgwp5sek4pjb4awy66sp184hrv
  63. foreign key (REV)
  64. references REVINFO
  65. alter table Address_AUD
  66. add constraint FK52pqkpismfxg2b9tmwtncnk0d
  67. foreign key (REVEND)
  68. references REVINFO
  69. alter table Country_AUD
  70. add constraint FKrix4g8hm9ui6sut5sy86ujggr
  71. foreign key (REV)
  72. references REVINFO
  73. alter table Country_AUD
  74. add constraint FKpjeqmdccv22y1lbtswjb84ghi
  75. foreign key (REVEND)
  76. references REVINFO
  77. alter table Customer
  78. add constraint FKfok4ytcqy7lovuiilldbebpd9
  79. foreign key (address_id)
  80. references Address
  81. alter table Customer_AUD
  82. add constraint FK5ecvi1a0ykunrriib7j28vpdj
  83. foreign key (REV)
  84. references REVINFO
  85. alter table Customer_AUD
  86. add constraint FKqd4fy7ww1yy95wi4wtaonre3f
  87. foreign key (REVEND)
  88. references REVINFO

21.21. Mapping exceptions

21.21.1. What isn’t and will not be supported

Bags are not supported because they can contain non-unique elements. Persisting a bag of `String`s violates the relational database principle that each table is a set of tuples.

In case of bags, however (which require a join table), if there is a duplicate element, the two tuples corresponding to the elements will be the same. Although Hibernate allows this, Envers (or more precisely the database connector) will throw an exception when trying to persist two identical elements because of a unique constraint violation.

There are at least two ways out if you need bag semantics:

  1. use an indexed collection, with the @javax.persistence.OrderColumn annotation.

  2. provide a unique id for your elements with the @CollectionId annotation.

21.21.2. What isn’t and will be supported

  • Bag style collections with a @CollectionId identifier column (see HHH-3950).

21.22. @OneToMany with @JoinColumn

When a collection is mapped using these two annotations, Hibernate doesn’t generate a join table. Envers, however, has to do this so that when you read the revisions in which the related entity has changed, you don’t get false results.

To be able to name the additional join table, there is a special annotation: @AuditJoinTable, which has similar semantics to JPA @JoinTable.

One special case is to have relations mapped with @OneToMany with @JoinColumn on the one side, and @ManyToOne and @JoinColumn( insertable = false, updatable = false) on the many side. Such relations are, in fact, bidirectional, but the owning side is the collection.

To properly audit such relations with Envers, you can use the @AuditMappedBy annotation. It enables you to specify the reverse property (using the mappedBy element). In case of indexed collections, the index column must also be mapped in the referenced entity (using @Column( insertable = false, updatable = false ), and specified using positionMappedBy. This annotation will affect only the way Envers works. Please note that the annotation is experimental and may change in the future.

21.23. Advanced: Audit table partitioning

21.24. Benefits of audit table partitioning

Because audit tables tend to grow indefinitely, they can quickly become really large. When the audit tables have grown to a certain limit (varying per RDBMS and/or operating system) it makes sense to start using table partitioning. SQL table partitioning offers a lot of advantages including, but certainly not limited to:

  1. Improved query performance by selectively moving rows to various partitions (or even purging old rows).

  2. Faster data loads, index creation, etc.

21.25. Suitable columns for audit table partitioning

Generally, SQL tables must be partitioned on a column that exists within the table. As a rule, it makes sense to use either the end revision or the end revision timestamp column for partitioning of audit tables.

End revision information is not available for the default AuditStrategy.

Therefore the following Envers configuration options are required:

org.hibernate.envers.audit_strategy = org.hibernate.envers.strategy.ValidityAuditStrategy

org.hibernate.envers.audit_strategy_validity_store_revend_timestamp = true

Optionally, you can also override the default values using following properties:

org.hibernate.envers.audit_strategy_validity_end_rev_field_name

org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name

For more information, see Configuration Properties.

The reason why the end revision information should be used for audit table partitioning is based on the assumption that audit tables should be partitioned on an ‘increasing level of relevancy’, like so:

  1. A couple of partitions with audit data that is not very (or no longer) relevant. This can be stored on slow media, and perhaps even be purged eventually.

  2. Some partitions for audit data that is potentially relevant.

  3. One partition for audit data that is most likely to be relevant. This should be stored on the fastest media, both for reading and writing.

21.26. Audit table partitioning example

In order to determine a suitable column for the ‘increasing level of relevancy’, consider a simplified example of a salary registration for an unnamed agency.

Currently, the salary table contains the following rows for a certain person X:

Table 12. Salaries table
YearSalary (USD)

2006

3300

2007

3500

2008

4000

2009

4500

The salary for the current fiscal year (2010) is unknown. The agency requires that all changes in registered salaries for a fiscal year are recorded (i.e., an audit trail). The rationale behind this is that decisions made at a certain date are based on the registered salary at that time. And at any time it must be possible to reproduce the reason why a certain decision was made at a certain date.

The following audit information is available, sorted in order of occurrence:

Table 13. Salaries - audit table
YearRevision typeRevision timestampSalary (USD)End revision timestamp

2006

ADD

2007-04-01

3300

null

2007

ADD

2008-04-01

35

2008-04-02

2007

MOD

2008-04-02

3500

null

2008

ADD

2009-04-01

3700

2009-07-01

2008

MOD

2009-07-01

4100

2010-02-01

2008

MOD

2010-02-01

4000

null

2009

ADD

2010-04-01

4500

null

21.27. Determining a suitable partitioning column

To partition this data, the level of relevancy must be defined. Consider the following:

  1. For the fiscal year 2006, there is only one revision. It has the oldest revision timestamp of all audit rows, but should still be regarded as relevant because it’s the latest modification for this fiscal year in the salary table (its end revision timestamp is null).

    Also, note that it would be very unfortunate if in 2011 there would be an update of the salary for the fiscal year 2006 (which is possible until at least 10 years after the fiscal year), and the audit information would have been moved to a slow disk (based on the age of the revision timestamp). Remember that, in this case, Envers will have to update the end revision timestamp of the most recent audit row.

  2. There are two revisions in the salary of the fiscal year 2007 which both have nearly the same revision timestamp and a different end revision timestamp.

On first sight, it is evident that the first revision was a mistake and probably not relevant. The only relevant revision for 2007 is the one with end revision timestamp value of null.

Based on the above, it is evident that only the end revision timestamp is suitable for audit table partitioning. The revision timestamp is not suitable.

21.28. Determining a suitable partitioning scheme

A possible partitioning scheme for the salary table would be as follows:

end revision timestamp year = 2008

This partition contains audit data that is not very (or no longer) relevant.

end revision timestamp year = 2009

This partition contains audit data that is potentially relevant.

end revision timestamp year >= 2010 or null

This partition contains the most relevant audit data.

This partitioning scheme also covers the potential problem of the update of the end revision timestamp, which occurs if a row in the audited table is modified. Even though Envers will update the end revision timestamp of the audit row to the system date at the instant of modification, the audit row will remain in the same partition (the ‘extension bucket’).

And sometime in 2011, the last partition (or ‘extension bucket’) is split into two new partitions:

  1. end revision timestamp year = 2010: This partition contains audit data that is potentially relevant (in 2011).

  2. end revision timestamp year >= 2011 or null: This partition contains the most interesting audit data and is the new ‘extension bucket’.

21.29. Envers links

  1. Hibernate main page

  2. Forum

  3. JIRA issue tracker (when adding issues concerning Envers, be sure to select the “envers” component!)

  4. Zulip channel

  5. FAQ