Advanced Auditing with Hibernate Envers

Introduction:

In the previous blog, we looked at different auditing approaches and their pros/cons. For basic auditing, JPA/Spring Data JPA should work. However, if you are looking for advanced auditing and looking to track the history of changes, what changed in a particular revision, how an entity looked like a particular revision etc., Envers is the way to go. It also provides a powerful query API to query the auditing data. In this blog, we will focus on how to do Advanced Auditing with Hibernate Envers.

Hibernate Envers

Hibernate Envers uses the concept of revisions, similar to how version control works. Below are the high level (simple) steps to enable auditing using Envers:

  1. Add Hibernate-Envers jar to your classpath
  2. Add @Audited annotation to the entity (for auditing all the columns in an entity) or to specific columns in an Entity
  3. Provides powerful AuditReader API for querying – Provides way to get all the revisions for a particular entity (or in a certain date range), way to see how an entity looked like at a particular revision etc.,

That’s it and you now have a way to track all the user activities in your database.

Auditing Strategies

Currently there are two auditing strategies provided by Hibernate envers. Both have their own pros and cons.

  1. Default Audit Strategy
  2. Validity Audit Strategy

Default Audit Strategy

Default audit strategy persists the audit data together with a start revision. For each row inserted, updated or deleted, one or more audit rows will be inserted in the audit table with the start revision. Rows in the audit tables are never updated after the insertion. Query on the audit tables uses subqueries to fetch the data from the audit table. Since the subquery is slow, fetching data from the audit table is very slow.

Validity Audit Strategy

Validity audit strategy stores both the start revision and end revision in audit table for each of the insert, update or delete. In this audit strategy, persisting the audit row is bit slower because of extra updates (due to updating end revision in previous record) on the inserted rows, but fetching the audit records is much faster (because we can use between start and end revision instead of using subquery) compared to default audit strategy.

We decided to go with validity audit strategy for better performance. Default in Envers is “DefaultAuditStrategy” and you can switch to “ValidityAuditStrategy” by providing the following Hibernate configuration property in persistence.xml:

<property name="org.hibernate.envers.audit_strategy"
          value="org.hibernate.envers.strategy.ValidityAuditStrategy"/>

Revision Table Structure

While inserting audit information, Envers will look for a table called “RevInfo”, which is a master table that contains the unique number for each revision across the application and the time stamp of that revision. It contains two mandatory fields called “Rev” and “RevTimeStamp” to hold these values. However, we can customize these tables to store other information like the LoginId, IPAddress etc., for each revision.

Below is an example RevInfo table:

RevRevTimeStampLoginId
12018-05-10 11:15:13Admin
22018-05-10 11:16:01Admin
32018-05-10 13:16:15Admin

 

Audit Table Structure

Envers will create separate audit table for each audited entity. The audit table is suffixed with “_AUD”, by default. For example, the audit table of a “user” entity will be like “user_AUD”. The audit table contains all the audited fields in entity table and some other fields will be introduced into the audit tables by envers itself to track the changes easily.

Audit tables contain these three additional fields “Rev”, “RevType” and “RevEnd”. Rev field is foreign key to master table “RevInfo” which holds the unique revision number for each audited row. RevType field values are taken from RevisionType enum and holds the values 0, 1 and 2 which represents the insertion of new row (ADD), updating the existing row (MOD) and deletion of the row (DEL) respectively. RevEnd field is present only if you are using validity audit strategy which holds the next revision number of that entity.

For example, consider a table called User having UserId, Name and EMailID fields. Then, audit table for this table will look like:

user_AUD

RevRevTypeRevEndUserIdNameEMailID
1021User 03user03@test.com
2131User 03user03@new.com
32NULL2User 04user04@test.com

 

Querying Audit Data

Envers queries are similar to hibernate criteria queries. If you are familiar with Hibernate queries, Envers queries would be easy to pick up. The main limitation of the current query implementation is we cannot traverse through the relational entities.

AuditReader

The AuditReaderFactory class contains static methods which opens the current session to create the query.

Give me all the revisions for user entity

List revisions = AuditReaderFactory.get(entityManager)
                                   .createQuery()
                                   .forRevisionsOfEntity(User.class, false, true)
                                   .getResultList();

the above query returns all the rows from user_AUD table along with respective revision data from revInfo table. Below is a sample output:

RevRevTimeStampLoginIdUserIdNameEMailIDRevType
12018-05-10 11:15:13Admin1User 03user03@test.com0
22018-05-10 11:16:01Admin1User 03user03@new.com1
32018-05-10 13:16:15Admin2User 04user03@test.com2

 

Give me how user entity looked like in a given revision

User user = (User) AuditReaderFactory.get(entityManager)
                                     .createQuery()
                                     .forEntitiesAtRevision(User.class, revisions.get(0))
                                     .getSingleResult();

Give me all revisions for user with primary key 1

AuditReader auditReader = AuditReaderFactory.get(entityManager);
List revisionNumbers = reader.getRevisions(User.class, 1);

Give me how user with primary key 1 looked like in a given revision

AuditReader auditReader = AuditReaderFactory.get(entityManager);
User user = auditReader.find(User.class, 1, revisions.get(1));

AuditEntity

AuditEntity is used to define constraints and projections on a query created from AuditReaderFactory.

Give me revisions where user name is User03

List revisions = AuditReaderFactory.get(entityManager)
                                   .createQuery()
                                   .forRevisionsOfEntity(User.class, false, true)
                                   .add(AuditEntity.property(Name)
                                   .eq(“User03”))
                                   .getResultList();

We can add different constraints and projections as per the use case to fetch the audit history of an entity. For complete reference, please refer Envers AuditReader JavaDoc.

Conclusion:

As you can see, Envers provides a powerful, easy to use API to track all the user activities in audit tables and a powerful query API to query the Audit History.