In a real world project you will need to audit certain database changes keeping track of the author of the change. Hibernate includes nowadays the Envers API and here I am showing you how to achieve auditing when using Hibernate as JPA persistence provider.
- Include dependencies. I can confirm this works with the below version. 3.5.1 for example fails to update auditing tables. About other versions you will need to try yourself.
<!-- Hibernate Audit --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> <version>3.5.6-Final</version> </dependency>
- In persistence.xml use hibernate.hbm2ddl.auto=”create” (I do not like update as it is dangerous to update your schema, I prefer creating it from scratch or failing if it already exists) to ensure tables are created from Java.
... <properties> <property name="hibernate.hbm2ddl.auto" value="create" /> ...
- And then add also the properties needed for audit
<!-- Audit --> <property name="hibernate.ejb.event.post-insert" value="org.hibernate.ejb.event.EJB3PostInsertEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-update" value="org.hibernate.ejb.event.EJB3PostUpdateEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-delete" value="org.hibernate.ejb.event.EJB3PostDeleteEventListener,org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-remove" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-collection-recreate" value="org.hibernate.envers.event.AuditEventListener" />
- Annotate your entities or fields with @Audited. Let us start with a simple example where you annotate specific fields
@Audited private String home_email;
- Now you will end up with two tables.
CREATE TABLE `revinfo` ( `rev` int(11) NOT NULL AUTO_INCREMENT, `revtstmp` bigint(20) DEFAULT NULL, PRIMARY KEY (`rev`) ) ENGINE=InnoDB AUTO_INCREMENT=3; CREATE TABLE `employee_aud` ( `id` int(11) NOT NULL, `rev` int(11) NOT NULL, `revtype` tinyint(4) DEFAULT NULL, `home_email` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`,`rev`), KEY `FK9900899F8B9EFD24` (`rev`), CONSTRAINT `FK9900899F8B9EFD24` FOREIGN KEY (`rev`) REFERENCES `custom_revision_entity` (`id`) ) ENGINE=InnoDB;
- Update the entity and you will get records in both tables. For example:
select * from employee_aud; +----+-----+---------+----------------+ | id | rev | revtype | home_email | +----+-----+---------+----------------+ | 79 | 1 | 1 | abc@sample.com | +----+-----+---------+----------------+ 1 row in set (0.00 sec) select * from revinfo; +-----+---------------+ | rev | revtstmp | +-----+---------------+ | 1 | 1300395363157 | +-----+---------------+ 1 row in set (0.02 sec)
- You might want to use the envers API to retrieve historical data but if you have to provide such data from reports you probably just want to use plain old SQL. For example see the below a query results after two updates for employee Paul:
SELECT e.first_name, ea.home_email, FROM_UNIXTIME(r.revtstmp / 1000) FROM employee e INNER JOIN employee_aud ea ON e.id = ea.id INNER JOIN revinfo r ON r.rev = ea.rev; +------------+-----------------+----------------------------------+ | first_name | home_email | FROM_UNIXTIME(r.revtstmp / 1000) | +------------+-----------------+----------------------------------+ | Paula | abc@sample.com | 2011-03-17 16:56:03 | | Paula | abcd@sample.com | 2011-03-17 17:04:49 | +------------+-----------------+----------------------------------+ 2 rows in set (0.00 sec)
- We are now in the middle of the task resolution as we need to provide the user that actually made the change. The first thing to be done is to create a JPA entity that will create a third table
import javax.persistence.Entity; import org.hibernate.envers.DefaultRevisionEntity; import org.hibernate.envers.RevisionEntity; @Entity @RevisionEntity(CustomRevisionListener.class) public class CustomRevisionEntity extends DefaultRevisionEntity { private static final long serialVersionUID = 3775550420286576001L; private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
- Here is the CustomRevisionListener
import org.hibernate.envers.RevisionListener; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; public class CustomRevisionListener implements RevisionListener { public void newRevision(Object revisionEntity) { CustomRevisionEntity revision = (CustomRevisionEntity) revisionEntity; UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); revision.setUsername(userDetails.getUsername()); } }
- Here is the new table (In production I include this as part of the deployment plan). Note that table revinfo will not longer exist. This is the new table containing revision details.
CREATE TABLE `custom_revision_entity` ( `id` int(11) NOT NULL AUTO_INCREMENT, `timestamp` bigint(20) NOT NULL, `username` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;
- Finally after some editions you can pull some report for your manager directly from SQL
SELECT e.first_name, ea.home_email, FROM_UNIXTIME(cre.timestamp / 1000), cre.username FROM employee e INNER JOIN employee_aud ea ON e.id = ea.id INNER JOIN custom_revision_entity cre ON cre.id = ea.rev; +------------+-----------------+-------------------------------------+-------------------+ | first_name | home_email | FROM_UNIXTIME(cre.timestamp / 1000) | username | +------------+-----------------+-------------------------------------+-------------------+ | Paula | abc@sample.com | 2011-03-18 11:44:47 | nurquiza@krfs.com | | Paula | abcd@sample.com | 2011-03-18 11:47:19 | nurquiza@krfs.com | +------------+-----------------+-------------------------------------+-------------------+ 2 rows in set (0.00 sec)
- You can annotate whole entities with the exceptions of certain fields. For example let’s say we want to audit the whole employee class with the exception of some ManyToOne and OneToMany fields but including at least a ManyToOne relationship
@Entity @Audited public class Employee{ ... @NotAudited @ManyToOne(fetch = FetchType.LAZY) private Office office; @NotAudited @OneToMany private List<EmployeePromotionHistory> promotionHistory; @ManyToOne(fetch = FetchType.LAZY) private Department department; ... }
- For the above to work you will need to audit the department entity or at least one of its fields:
@Entity public class Department { ... @Audited private String name; ... }
- Note that when you assign a different department to the employee a new record will appear in employee_aud table, however no records will show up in department_aud. When a department name is changed then the change is registered in department_aud. Here is where the AuditQuery comes handy in terms of queries.
From now on the audited entity will keep not only the history but also who changed it. Note that you can add other columns creating the possibility to achieve literally anything you want from an entity auditing perspective.
If you want to audit the user iterations you can take a look at my recent post about login requests. Instead of just login to a file you can and use a table where you keep by sessionId all request details. This has proven to be extremely useful for a company I worked for in the past. Thanks to this “flow logging” they basically can do BI on their clients, dispute client billing and so much more.
Source: http://thinkinginsoftware.blogspot.com/2011/03/auditing-entities-with-hibernate-jpa.html