Auditing/versioning for Hibernate entities with Envers



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!

 

Published May 6, 2010