Introduction
A lot of the projects need some kind of audit trail. They want to know who and when the last time a record was changed. Or recently we got a question how you could keep record of who read some data from the database.
Most of the time, there exists a database solution to these answers, but in other cases, it is easier to achieve with the JPA implementation.
This text describes 2 scenarios where you can use JPA to do some auditing on your users with the help of EntityListener.
EntityListener
The JPA EntityListener defines callbacks for the lifecycle of the entity. And as with any callback mechanism of a system, it allows you to extend the system with some generic functionality.
There are 7 callbacks defined, a before and after for persist, update and remove and one method called after the load is done.
- PrePersist
- PostPersist
- PreUpdate
- PostUpdate
- PreRemove
- PostRemove
- PostLoad
Keep record of reads
We recently got a request from a client who wanted to know which user at what time has read some data from specific tables.
One of the lifecycle callbacks is the javax.persistence.PostLoad. It gets called when JPA has read the record from the database and after it has been added to the context.
As we wanted to separate the code for this auditing requirement from the Entity code, we placed the annotation @PostLoad in a callback listener class. Something like the following code.
public class Audit { @PostLoad public void auditUsage(GenericEntity entity) { } }
The GenericEntity is an abstract class (see further on why it is not an interface) that all of our Entity classes implements. It contains, among others, a method getId() to retrieve the value of the Id field (primary key).
This Audit class is then defined on the entities we need to track with the use of the @EntityListener annotation.
@Entity @Table(name = "t_user") @EntityListeners({Audit.class}) public class Employee extends GenericEntity implements Serializable {
Our first idea was to separate the write of the audit information from the Audit class itself. A loose coupling could be achieved by using the CDI event mechanism.
But it turns out the CDI injection is not available in callback listener classes. The spec (JSR 338) specifies that CDI injection must be available, but both GlassFish and WildFly have issues with it.
But ‘injection’ of the EntityManager is possible and thus we can persist the information to the database from within the Audit class.
@PersistenceContext private EntityManager em; @PostLoad public void auditUsage(GenericEntity entity) { AuditRead auditRead = new AuditRead(entity.getClass().getSimpleName(), entity.getId()); em.persist(auditRead); }
Small remark, using the entity manager during a callback method is discouraged in the specification. But you are allowed to perform some JMS operation. That way you can also guarantee the correct logging of all read operations.
Record update information
With triggers on the database we can store the last time a record is changed. But the username (user which last updated the record) can’t be stored like this, because the application server connects with a generic application user to the database.
So the user name needs to be filled in by your application.
The @PreUpdate and @PrePersist lifecycle callbacks can be used for this purpose with the help of your security/user information.
The logged in user information is most of the time available in some CDI bean placed on the Session scope. As we already mentioned, CDI injection is buggy in the callback listener class.
You can retrieve your bean using one of the following ways
- Retrieve the BeanManager from JNDI and get your bean directly from it (various sources on the internet shows you the 4 lines of code that you need to retrieve a line from the BeanManager)
- Write a CDI extension that captures the bean manager for you, so that you don’t have to retrieve it every time from JNDI. And then perform the 4 lines mentioned above.
- Use the BeanProvider class from DeltaSpike where you can retrieve with a single line, the bean instance which implements a certain class (type to be exact)
Our GenericEntity abstract class has methods and fields to keep track of the user who created or last modified the entity. The database tables needs to have corresponding fields of course.
Conclusion
With the JPA EntityListeners we can handle most of the auditing requirements which we need in our applications. We can keep track of who is reading some records or who updated some data for the last time.
But beware of using of the callback listener classes. CDI injection is most of the time not implemented according to the specification document and EntityManager should be used wich caution here.
No comments:
Post a Comment