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 uses the concept of revisions, similar to how version control works. Below are the high level (simple) steps to enable auditing using Envers:
- Add Hibernate-Envers jar to your classpath
- Add @Audited annotation to the entity (for auditing all the columns in an entity) or to specific columns in an Entity
- 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.
Currently there are two auditing strategies provided by Hibernate envers. Both have their own pros and cons.
- Default Audit Strategy
- 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:
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:
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:
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.
The AuditReaderFactory class contains static methods which opens the current session to create the query.
Give me all the revisions for user entity
.forRevisionsOfEntity(User.class, false, true)
the above query returns all the rows from user_AUD table along with respective revision data from revInfo table. Below is a sample output:
|1||2018-05-10 11:15:13||Admin||1||User email@example.com||0|
|2||2018-05-10 11:16:01||Admin||1||User firstname.lastname@example.org||1|
|3||2018-05-10 13:16:15||Admin||2||User email@example.com||2|
Give me how user entity looked like in a given revision
Give me all revisions for user with primary key 1
List revisionNumbers = reader.getRevisions(User.class, 1);
Give me how user with primary key 1 looked like in a given revision
User user = auditReader.find(User.class, 1, revisions.get(1));
AuditEntity is used to define constraints and projections on a query created from AuditReaderFactory.
Give me revisions where user name is User03
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.
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.