My web applicaton architecture:
Spring 2.5
Hibernate 3.5.3 (Envers bundled)
Struts 1.3
Mysql
Jboss AS
Hibernate Envers :
Recently I completed an R&D over audit logging for our Financial Application, a very needed feature of most financial services applications. I came across an article just like this which really helped me a lot in learning about Envers, But in my case I had something extra in my web application, Spring 2,5. So I thought of writing this post to help people who might have the same configuration like mine.
In my RnD I worked on Hibernate Interceptor to create an audit logging utility. Hibernate Envers does pretty much the same and much more. So what is Envers? Envers was initially a JBoss project and as of Hibernate 3.5 it is part of the hibernate. In simple terms for every pojo/entity that needs to be Versioned or Audited (terms change from the first version to the version added into hibernate) it maintains a revision of what was added or changed in its own tables. This is very beneficial as audit data and main application data remains in separate tables.
I have tried Envers with both the first ever version that came out i.e Envers 1.0.0 with hibernate 3.2.5 and with the latest Hibernate 3.5.3 which has the latest Envers bundled in one library. I had to do this because we were initially using hibernate 3.2.4 but because of some limitations I chose to switch our hibernate to latest.
Envers is fairly simple to implement if one goes through their tutorial. The only difference on my end was that we were using Spring and the hibernate support provided by spring for persistence layer. Some of the challenges that I met were typical. In both the Envers versions, the steps are the same with only differences in terminology and the actual event listener class used. For example, the annotation used in the first version is @Versioned and in the newer version it is @Audited. Similarly, the event listener is also different. for the first version it is "org.jboss.envers.event.VersionsEventListener" while this changes to "org.hibernate.envers.event.AuditEventListener" when it has been bundled up in hibernate 3.5.3.
In order to implement Envers, just make sure you have the envers library in your lib folder, add the @Versioned/@Audited annotation on the entity/pojo you want to be audited and register the persitence events like post-update, post-insert and post-delete with the respective eventlistener. (See Listing 3 AuditEventListener below).
I think it would be beneficial if a bit about envers is also explained, as this is the purpose of this article as well, and what it creates after its implementation. First thing that is created is a global revision table("_revisions_info"). By default, this table is created by hibernate, again, the nomenclature is different from both versions. In first version its called _revison_info and in the latest its called REVINFO table. It doesn't matter what name it is, in my case as I started with the first version I got the _revision_info table and when I switched over to the newer version i kept the same name. This was also because I had customized the RevisionEntity class that represented the global revision table and it was mapped to this same name. The RevisionEntity class is mapped to the global table. If you extend the RevisionEntity to add fields to the global revisions table then you will have to create a hbm.xml mapping for hibernate to know which table this class refers to.
I
n practice, you don't add non business/domain model fields in domain tables, like username,timsetamp or enviroment variables from where the change was initiated. These belong in the global revision table as they are all related to audit trail functionality. The custom revision entity is just like any other pojo with only one difference. It is annotated with @RevisionEntity(Listener.class) . This tells hibernate that this is the revision entity class bind to this listener. This listener class is which you create in order to fill the custom fields that you added to the revision entity. These two customized classes are not needed if you dont want to add any thing more to the audit trail by envers. As it was a requirement to add a username and the screen name related to the change to be shown in audit trail I made these two classes.
In order to capture the username in the listener you would need an authetication layer which utilizes threadlocal to keep the username in the same execution context. For that I used spring security. Again, it was very simple to implement. Though there is a steep learning curve needed to start but when you get the hang of it you can easily implement it. Spring security provides a SecurityContextHolder that retains the authentication principal. I utilized that to get the username. Screen name was bit difficult. For that I needed to customize the spring security, So i will leave those details for now.
Listing 1 Custom Revision Entity:
/**
* @author armahdi
*
*/
@RevisionEntity(CustomEnversListener.class)
public class CustomRevisionEntity {
@RevisionNumber
private int id;
@RevisionTimestamp
private long timestamp;
private String username;
private String screenName;
public String getScreenName() {
return screenName;
}
public void setScreenName(String screenName) {
this.screenName = screenName;
}
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
CREATE TABLE `_revisions_info` (
`revision_id` int(11) NOT NULL AUTO_INCREMENT,
`revision_timestamp` bigint(20) DEFAULT NULL,
`username` varchar(50) DEFAULT NULL,
`screenname` varchar(50) DEFAULT NULL,
PRIMARY KEY (`revision_id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=latin1
Listing 2 Revision Listener:
/**
* @author armahdi
*
*/
public class CustomEnversListener implements RevisionListener {
/* (non-Javadoc)
* @see org.jboss.envers.RevisionListener#newRevision(java.lang.Object)
*/
@Override
public void newRevision(Object revisionEntity) {
CustomRevisionEntity customRevisionEntity = (customRevisionEntity)revisionEntity;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
customRevisionEntity.setUsername(authentication.getName());
//customRevisionEntity.setScreenName("myscreen");
}
}
In order to listen to events we have to register an envers event listener to specific events, You can place the configuration below in Hibernate.cfg.xml, in my case as we were using spring to configure session factory I placed them in spring-config.xml, I had also placed them in Hibernate.cfg.xml where my mapping resources were present and it also worked fine :
Listing 3 AuditEventListener:
<bean id="envers" class="org.hibernate.envers.event.AuditEventListener" />
<property name="eventListeners">
<map>
<entry key="post-insert" value-ref="envers"/>
<entry key="post-update" value-ref="envers"/>
<entry key="post-delete" value-ref="envers"/>
<entry key="post-collection-recreate" value-ref="envers"/>
<entry key="pre-collection-remove" value-ref="envers"/>
<entry key="pre-collection-update" value-ref="envers"/>
</map>
</property>
Listing 4 The table created by Envers for domain table has two fields added to the _aud table:
CREATE TABLE `purchase` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`PurschaseDate` date NOT NULL,
`RecieptNo` varchar(40) DEFAULT NULL,
`OrderNo` varchar(40) DEFAULT NULL,
`Amount` decimal(16,4) NOT NULL,
`CustomerId` int(11) NOT NULL,
`PurchaseTypeId` int(11) NOT NULL,
PRIMARY KEY (`Id`),
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1
CREATE TABLE `purchase_aud` (
`Id` int(11) NOT NULL,
`REV` int(11) NOT NULL,
`REVTYPE` tinyint(4) DEFAULT NULL,
`PurchaseDate` date Default NULL,
`OrderNo` varchar(40) DEFAULT NULL,
`RecieptNo` varchar(40) DEFAULT NULL,
`Amount` decimal(16,4) DEFAULT NULL,
`CustomerId` int(11) DEFAULT NULL,
`PurchaseTypeId` int(11) DEFAULT NULL,
PRIMARY KEY (`Id`,`REV`),
KEY `FK2BEB2098BF3453E` (`REV`),
CONSTRAINT `FK2BEB2098BF3453E` FOREIGN KEY (`REV`) REFERENCES `_revisions_info` (`revision_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Notice the REV and REVTYPE columns in the _AUD table, REV is the revision number which is referenced from the _revision_info global revision table while REVTYPE is an audit code which represents what type of change had occured in a particular revision, REVTYPE 0 denotes an insert, 1 an update and 2 a delete.
For REVTYPE 0 it will insert the same row as inserted the main domain table. For 1 it will do the same as 0 and add the row that was updated. For revtype 2, delete, it has two options that can be enabled in hibernate properties. By default it will insert an empty row of the fomain table with only revtype 2 and revisionId and the id of the deleted entity. The other option can be turned on to actually fill the _AUD table row with the row it has just deleted.
Challenges that I faced:
1)We were using org.springframework.orm.hibernate3.LocalSessionFactoryBean to handle the hibernate's session factory. Most projects use this as the basic session factory bean, i believe. As envers is applied through annotations, this was giving exceptions so after some research I changed to org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean, this worked perfectly fine.
2)When an entity is audited any other entities that are related to that entity and are referenced by, lets say, id must be audited as well. There is an option of using @NotAudited annotation but I haven't tried it myself on referenced entities.
3)I found it cumbersome to produce all the _aud tables myself and I wanted hibernate to do that for me. I used
Listing 5 Auto Table Creation:
<property name="hibernate.hbm2ddl.auto">create-drop</property>
property in hibernate.cfg.xml on my first run and produce those tables. Once created I copied their CREATE sqls and turned off this feature. This was because on every hot deploy of my application all of the data was wiped out. So instead of debugging this minor issue, I just copied the create sqls to save time as our tables in the database were usually generated by an sql file and not through hibernate.
4)When everything was setup correctly the last but a critical issue that arose was that _aud table was not being filled with anything. It seemed that the events were not being registered. In order to tell Spring about the events registered in AuditEventListener listing above we had to use @Transactional annotation on the service classes. I am not sure how I would have done it in a non spring environment in this case. This @Transactional annotation saved a lot of time.
5)In many to one relationship, our relationships were not mapped correctly with the Id of the related table, So one needs to be wary of these relationships and how they are referencing each other in their domain objects.
Creating an Audit Trail Report:
Implementing Envers was an easy part, to actually use the _aud tables is where the fun is. For that envers provides an AuditReader to actually traverse through the revisions. Auditreader, lets developers traverse(also known as starf) horizontally and vertically through audit data.
Horizontal: is the state of the any row of any table at a given revision. Thus, we can query for entities as they were at revision N. Vertical: are the revisions, at which entities changed. Hence, we can query for revisions, in which a given entity changed.
To create an AuditReader you can use a hibernate session or an EntityManager. In my case I used the session. After creation of AuditReader, it is pretty easy to create querries to fetch required revisions, especially if you are familiar to hibernate criteria. Even if you are not, it doesn't take time to get used to this style.
Listing 6 A Sample:
/**
* This code snippet resides in a method of the Dao layer. My DapImpl extends
* org.springframework.orm.hibernate3.support.HibernateDaoSupport
*
*/
List<?> list = null;
Session session = this.getSession();// this being HibernateDaoSupport
AuditReader reader = AuditReaderFactory.get(session);
AuditQuery query=reader.createQuery().forRevisionsOfEntity(Purchase.class, false, true);
//if you have added your username field in the RevisionEntity/_revision_info you should use this:
query.add(AuditEntity.revisionProperty("username").eq(userName));
//If you have added username Field to the purchase entity/pojo/table use this:
query.add(AuditEntity.property("username").eq(userName));
list = query.getResultList();
See its that simple!!!.
Conclusion:
I found Envers very easy to implement and powerful to iterate through the revisions and query the historical data. I would really like if there are any feedbacks if I could have done things in much simpler way.
Comments