Easy auditing/versioning for your Hibernate entities with Envers

Posted on May 6, 2010 (3 years ago). Seen 26,134 times. 4 comments. Permalink Feed
Photo Laurent Tonon
Software Developer
Marakana, Inc.
Member since Apr 9, 2010
Location: San Francisco
Stream Posts: 14
Tagged as: Hibernate Java Tutorial
Hello everyone,

Do you want to track modifications of your objects? See what has been created, modified, deleted and when?

Let say you are creating a Wiki. You might want to have an history of your articles.

Nowadays, there is many way to implement that :
  • Create triggers on your database (High database dependency, so it's not a good idea).
  • Use Hibernate interceptors.
  • Use Hibernate Event listeners.
  • Hibernate Envers


Envers is library for Hibernate that will help us to easily achieve audit functionality. This has been created by Adam Warski. This is now part of Hibernate core 3.5.

Advantages :
  • Few modifications to the code (@Audited annotation in your classes and add listeners to your configuration file).
  • Database independent.
  • Use Envers wherever Hibernate works.
  • Gain of productivity (built-in methods to query entities history).
  • Querying in Envers is similar to Hibernate Criteria.


This tutorial is a short introduction to Envers. At the end of this tutorial you will know how to configure your project for Envers, audit your entities and, retrieve a version of an entity.

I assume in this tutorial that you know Java and basics of Hibernate (we will just audit one entity here).

I use hibernate-distribution-3.3.0.GA, hibernate-annotations-3.4.0.GA and, Envers-1.2.2.GA-hibernate-3.3

Here is the list of libraries you will need (hibernate3.jar, antlr-2.7.6.jar, commons-collections-3.1.jar, dom4j-1.6.1.jar, javassist-3.4.GA.jar, jta-1.1.jar, hibernate-annotations.jar, ejb3-persistence.jar, hibernate-commons-annotations.jar, mysql-connector-java-5.1.12-bin.jar, slf4j-api-1.5.11.jar, slf4j-jdk14-1.5.11.jar and, envers-1.2.2.ga-hibernate-3.3.jar).

You can download Hibernate Envers from here.

Consider this class as an example (User.java) with the mapping for your properties (remember that you cannot mix mappings, either on getters or on fields).

I annotate it with @Audited to tell Envers that this class has to be audited. All the fields will be audited except those annotated with @NotAudited (in my case, I have a password field that I don't want to audit) :

User.java:

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Entity;

import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;

@Entity
@Audited
public class User {

private Long id;
private String firstName;
private String lastName;
private String password;

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

@Column(length=20)
public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

@Column(length=20)
public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

@NotAudited
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

}


We now have to define in hibernate.cfg.xml the event listeners. This will be used by Envers to create new entries in the audit table on entity creation, modification, deletion. Listeners have to be defined at the end of your configuration file :

hibernate.cfg.xml:

...
<hibernate-configuration>
<session-factory>
...
<!-- Your configuration -->

<!-- This map our User class -->
<mapping class="com.marakana.testenvers.domain.User"/>

<!-- Envers Configuration comes HERE -->
<listener class="org.hibernate.envers.event.AuditEventListener" type="post-insert"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="post-update"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="post-delete"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="pre-collection-update"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="pre-collection-remove"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="post-collection-recreate"/>

</session-factory>
</hibernate-configuration>


Here is some properties in hibernate.cfg.xml you might want to take a look at :
  • org.hibernate.envers.auditTablePrefix and org.hibernate.envers.auditTableSuffix properties are useful to respectively prefix or suffix your audit tables. Default name of audit tables is tableName_AUD.
  • org.hibernate.envers.doNotAuditOptimisticLockingField property help to not audit the optimistic locking field (annotated with version) when set to true.


Create a HibernateUtil.java class in a package of your choice to have access to the SessionFactory and Session objects :

HibernateUtil.java:

import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;

public class HibernateUtil {

private static final SessionFactory sessionFactory = buildSessionFactory();

private static SessionFactory buildSessionFactory() {
try {
// Create the SessionFactory from Annotation
return new AnnotationConfiguration().configure().buildSessionFactory();
}
catch (Throwable ex) {
// Make sure you log the exception, as it might be swallowed
System.err.println("Initial SessionFactory creation failed." + ex);
throw new ExceptionInInitializerError(ex);
}
}

public static SessionFactory getSessionFactory() {
return sessionFactory;
}
}


I also quickly (No exception handling here) created a DAO to operate basic operations on our entities :

UserDao.java:

import org.hibernate.Session;

public class UserDao {

//I don't handle exceptions here, the point is to focus on Envers not on Hibernate.
//I want to go quickly to the point.

public void addOrEditObject(User u) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.saveOrUpdate(u);
session.getTransaction().commit();
}

public void deleteById(Long id) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
User u = (User) session.get(User.class, id);
session.delete(u);
session.getTransaction().commit();
}

public void deleteObject(User u) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
session.delete(u);
session.getTransaction().commit();
}

public User getObject(Long id) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
User u = (User) session.get(User.class, id);
session.getTransaction().commit();
return u;
}

}


Now it's time to create, modify some objects and see what happens.

Create an User :

Code:

User u = new User();
u.setFirstName("Laurent");
u.setLastName("Tonon");
u.setPassword("MyPassword123");
UserDao userDAO = new UserDao();
userDAO.addOrEditUser(u);


Hibernate generate these SQL statements :
Code:
insert into User (firstName, lastName, password) values (?, ?, ?)
insert into REVINFO (REVTSTMP) values (?)
insert into User_AUD (REVTYPE, firstName, lastName, id, REV) values (?, ?, ?, ?, ?)


What does your tables contains now ?
Code:

mysql> select * from user;
+----+-----------+----------+---------------+
| id | firstName | lastName | password |
+----+-----------+----------+---------------+
| 1 | Laurent | Tonon | MyPassword123 |
+----+-----------+----------+---------------+
1 row in set (0.03 sec)

mysql> select * from user_aud;
+----+-----+---------+-----------+----------+
| id | REV | REVTYPE | firstName | lastName |
+----+-----+---------+-----------+----------+
| 1 | 1 | 0 | Laurent | Tonon |
+----+-----+---------+-----------+----------+
1 row in set (0.00 sec)

mysql> select * from revinfo;
+-----+---------------+
| REV | REVTSTMP |
+-----+---------------+
| 1 | 1273189523203 |
+-----+---------------+
1 row in set (0.00 sec)

Two tables were created :
  • user_aud : This will contain all modification, creation and deletion of our entities. The primary key is composed of id and REV fields.
  • revinfo : Contains revision timestamp.


You can also see that specifying the password field with @NotAudited result in no auditing this field.

How do you know it's an object creation? Look at the REVTYPE column :
  • 0 means creation.
  • 1 means modification.
  • 2 means deletion.


Let's modify our object :

Code:

UserDao userDAO = new UserDao();
User u = (User) userDAO.getUser(new Long(1));
u.setFirstName("Indiana");
u.setLastName("Jones");
u.setPassword("123NewPassword");
userDAO.addOrEditUser(u);


Hibernate generate then these SQL statements :
Code:
update User set firstName=?, lastName=?, password=? where id=?
insert into REVINFO (REVTSTMP) values (?)
insert into User_AUD (REVTYPE, firstName, lastName, id, REV) values (?, ?, ?, ?, ?)


This modification will be recorded in database :
Code:
mysql> select * from user;
+----+-----------+----------+----------------+
| id | firstName | lastName | password |
+----+-----------+----------+----------------+
| 1 | Indiana | Jones | 123NewPassword |
+----+-----------+----------+----------------+
1 row in set (0.00 sec)

mysql> select * from user_aud;
+----+-----+---------+-----------+----------+
| id | REV | REVTYPE | firstName | lastName |
+----+-----+---------+-----------+----------+
| 1 | 1 | 0 | Laurent | Tonon |
| 1 | 2 | 1 | Indiana | Jones |
+----+-----+---------+-----------+----------+
2 rows in set (0.00 sec)

mysql> select * from revinfo;
+-----+---------------+
| REV | REVTSTMP |
+-----+---------------+
| 1 | 1273189523203 |
| 2 | 1273190158250 |
+-----+---------------+
2 rows in set (0.00 sec)


Look that now REVTYPE value equals 1.

How to retrieve an object in a previous version?

I will use here two methods from Hibernate Envers AuditReader AuditReaderFactory.get(Session sess) and Object AuditReader.find(Class<T> cls, Object primaryKey, Number revision) to get my User entity at the previous version (version 1).

Code:

AuditReader reader = AuditReaderFactory.get(HibernateUtil.getSessionFactory().openSession());
User u = (User) reader.find(User.class, new Long(1), 1);

System.out.println(u.getFirstName() + " " + u.getLastName());


This will display "Laurent Tonon". We retrieve these values from the user_aud table.

How to list the versions number of an entity ?

I will use the AuditReader method List<Number> getRevisions(Class<?> cls, Object primaryKey) :

Code:

List<Number> versions = reader.getRevisions(User.class, new Long(1));
for (Number number : versions) {
System.out.print(number + " ");
}


This will display : 1 2

Delete your entity :
Code:

UserDao userDAO = new UserDao();
userDAO.deleteById(new Long(1));


These are the SQL statements generated by Hibernate :
Code:

select user0_.id as id0_0_, user0_.firstName as firstName0_0_, user0_.lastName as lastName0_0_, user0_.password as password0_0_ from User user0_ where user0_.id=?
delete from User where id=?
insert into REVINFO (REVTSTMP) values (?)
insert into User_AUD (REVTYPE, firstName, lastName, id, REV) values (?, ?, ?, ?, ?)


What do we have in the database now ?
Code:
mysql> select * from user;
Empty set (0.00 sec)

mysql> select * from user_aud;
+----+-----+---------+-----------+----------+
| id | REV | REVTYPE | firstName | lastName |
+----+-----+---------+-----------+----------+
| 1 | 1 | 0 | Laurent | Tonon |
| 1 | 2 | 1 | Indiana | Jones |
| 1 | 3 | 2 | NULL | NULL |
+----+-----+---------+-----------+----------+
3 rows in set (0.00 sec)

mysql> select * from revinfo;
+-----+---------------+
| REV | REVTSTMP |
+-----+---------------+
| 1 | 1273189523203 |
| 2 | 1273190158250 |
| 3 | 1273248257046 |
+-----+---------------+
3 rows in set (0.00 sec)


You can see that even if the entity is deleted (no record in the user table), we still have records of our entity in the user_aud table.

So far you have seen how to setup a project for Envers, how to audit your classes and how to retrieve a version of an entity in the audit table.

See how easy and fast it is to implement basic Auditing on entities.

Next tutorial is on how to control insertions on audit tables.

You can also find help on :

Cheers!

Comments

Posted on May 11, 2010
Photo Laurent Tonon
Software Developer
Marakana, Inc.
Member since Apr 9, 2010
Location: San Francisco
Now the question you might have in mind is how to control insertions in audit tables?

We might want to only insert in audit tables what is interesting for our needs.

In this example I want to add a new field to my User class. This field called enabled will be a boolean. When this boolean equals false, I don't want to insert records in the audit table.

Do you remember these listeners in our hibernate.cfg.xml :
hibernate.cfg.xml:

<listener class="org.hibernate.envers.event.AuditEventListener" type="post-insert"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="post-update"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="post-delete"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="pre-collection-update"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="pre-collection-remove"/>
<listener class="org.hibernate.envers.event.AuditEventListener" type="post-collection-recreate"/>


These lines basically allow Hibernate Envers to "listen" for some events like insertion, modification or deletion of objects and perform some actions (insert in audit tables for instance).

If you want to override the default behavior, you have to create an event listener.

First of all, let's see what changes have been made in User.java :
User.java:

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Entity;

import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;

@Entity
@Audited
public class User {

private Long id;
private String firstName;
private String lastName;
private String password;
public boolean enabled;

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

@Column(length=20)
public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

@Column(length=20)
public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

@NotAudited
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}


}


Remember that in this example if the User is disabled we don't want to insert entries in the audit table.

To do that I create MyListener.java which extends from org.hibernate.envers.event.AuditEventListener :
MyListener.java:

import org.hibernate.envers.event.AuditEventListener;
import org.hibernate.event.PostCollectionRecreateEvent;
import org.hibernate.event.PostDeleteEvent;
import org.hibernate.event.PostInsertEvent;
import org.hibernate.event.PostUpdateEvent;
import org.hibernate.event.PreCollectionRemoveEvent;
import org.hibernate.event.PreCollectionUpdateEvent;

public class MyListener extends AuditEventListener {

@Override
public void onPostDelete(PostDeleteEvent arg0) {
super.onPostDelete(arg0);
}

@Override
public void onPostInsert(PostInsertEvent arg0) {
Object o = arg0.getEntity();

if (o instanceof User) {
User u = (User) o;
if (u.isEnabled()) {
super.onPostInsert(arg0);
} else {
System.out.println("User is disabled so I don't want to audit this change");
}
} else {
super.onPostInsert(arg0);
}

}


@Override
public void onPostRecreateCollection(PostCollectionRecreateEvent event) {
super.onPostRecreateCollection(event);
}

@Override
public void onPostUpdate(PostUpdateEvent arg0) {
Object o = arg0.getEntity();

if (o instanceof User) {
User u = (User) o;
if (u.isEnabled()) {
super.onPostUpdate(arg0);
} else {
System.out.println("User is disabled so I don't want to audit this change");
}
} else {
super.onPostUpdate(arg0);
}
}


@Override
public void onPreRemoveCollection(PreCollectionRemoveEvent event) {
super.onPreRemoveCollection(event);
}

@Override
public void onPreUpdateCollection(PreCollectionUpdateEvent event) {
super.onPreUpdateCollection(event);
}

}


Once the listener is created, we have to change the path to the listener class inside hibernate.cfg.xml :
Code:

<listener class="com.marakana.testenvers.mylisteners.MyListener" type="post-insert"/>
<listener class="com.marakana.testenvers.mylisteners.MyListener" type="post-update"/>
<listener class="com.marakana.testenvers.mylisteners.MyListener" type="post-delete"/>
<listener class="com.marakana.testenvers.mylisteners.MyListener" type="pre-collection-update"/>
<listener class="com.marakana.testenvers.mylisteners.MyListener" type="pre-collection-remove"/>
<listener class="com.marakana.testenvers.mylisteners.MyListener" type="post-collection-recreate"/>


Basically, if we want to record insertion or modification in our audit table, we call respectively void onPostInsert(PostInsertEvent arg0) or void onPostUpdate(PostUpdateEvent arg0) of the super class using the keyword super.

Now, let's add a User with disabled property set to false:
Code:

User u = new User();
u.setFirstName("Laurent");
u.setLastName("Tonon");
u.setPassword("abc123");
u.setEnabled(false);
UserDao userDAO = new UserDao();
userDAO.addOrEditObject(u);


Is our audit table still recording even if the User is disabled ?

Look what the console is saying :
Code:

Hibernate: insert into User (enabled, firstName, lastName, password) values (?, ?, ?, ?)
User is disabled so I don't want to audit this change


What is inside the database ?
Code:

mysql> select * from user;
+----+---------+-----------+----------+----------+
| id | enabled | firstName | lastName | password |
+----+---------+-----------+----------+----------+
| 1 | | Laurent | Tonon | abc123 |
+----+---------+-----------+----------+----------+
1 row in set (0.00 sec)

mysql> select * from user_aud;
Empty set (0.01 sec)

mysql> select * from revinfo;
Empty set (0.00 sec)


As we can all expect, the user_aud and revinfo tables are empty. Our custom listener has performed the test on the enabled property and has made his decision.

A question you might have in mind is how to delete audit entries when we delete an entity?

Well, this action would take place inside the onPostDelete of our listener.

I wrote a HQL query saying that I want to delete audit entries when I delete an entity :
Code:

@Override
public void onPostDelete(PostDeleteEvent arg0) {
Object o = arg0.getEntity();
if (o instanceof User) {
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction tx = null;
try {
tx = session.beginTransaction();
User u = (User) o;
Query q = session.createQuery("delete from com.marakana.testenvers.domain.User_AUD u where u.originalId.id = :userid");

q.setLong("userid", u.getId());
q.executeUpdate();

tx.commit();
} catch (Exception e) {
if (tx != null)
tx.rollback();
}
}
}


The "problem" is that it won't delete entries in revinfo table.

An entry in the revinfo table is created when a transaction is made.
The revinfo table containing revision numbers is shared by all audit tables because many object can be created in a transaction.

You can see that deleting audit entries is not easy at all, and this is not the purpose of auditing. That why Hibernate Envers doesn't provide built-in methods to delete audit entries.

So far, you have seen how can we configure our own listener and, you also have seen that deleting audit entries defeat the purpose of auditing.

Cheers!
Posted on Jun 18, 2011
Photo Anand Devaraj
Sr. Senior Engineer
CG-VAK Software and Exports
Member since Jun 18, 2011
Very Nice forum. Thanks. I learned the basic of Hibernate Envers and helped me. Keep on doing good work. Thanks a lot.
Posted on Jan 11, 2012
Photo Sudheer Kategar
consultant
Busineess
Member since Jan 11, 2012
Can I know how envers are used for keeping track of date/time of modifications. i.e., how to add previous date of modification and date on which it is modified.

Thanks in advance
Ramesh
Posted on Aug 2, 2012
Photo Surendra Manikpuri
DST
Member since Jun 15, 2012
Thanks!