Sunday, August 7, 2011

Using Hibernate's event architecture to manage created and updated timestamp fields

This blog post describes a method for managing created and last modified timestamp table columns using Hibernate 3.6.  Before I detail the working solution, I'll explain a failed approach I attempted.  I'm including this approach because it turned out to be useful learning experience.  You can skip to the bottom section of this post if you want to go straight to the working solution. :)

A little Googling found a Stack Overflow post suggesting using @Prepersist and @PreUpdate JPA Annotations.  Up to this point I'd been using Hibernate mapping files (hbm.xml) to define my entities.  I'd been using Ant and the hibernatetool task to generate both the POJO classes and the Database tables.  For the most part I was satisfied using the hbm xml mappings although I had planned on eventually converting to JPA Annotations mappings anyway.

The @Prepersist and @PreUpdate annotations looked simple enough and I decided now was time to bite the bullet and I convert all my .hbm xml mappings to JPA Annotations.  This took some time but was a worthwhile experience.  Maybe I'll write a blog post discussing the process I used for migrating from .hbm xml mapping files to JPA Annotations.

When I was finally satisfied my newly converted JPA Annotated classes were functioning as expected I went ahead and added @Prepersist and @PreUpdate to my entity POJOs as follows:

@Temporal(TemporalType.TIMESTAMP)
@Column(name="created", nullable = false)
public Date getCreated() {return created;}
public void setCreated(Date created) {this.created = created;}
private Date created;

@Temporal(TemporalType.TIMESTAMP)
@Column(name="updated", nullable = false)
public Date getUpdated() {return updated;}
public void setUpdated(Date updated) {this.updated = updated;}
private Date updated;

@PrePersist
protected void onCreate() {
setCreated(new Date());
setUpdated(new Date());}

@PreUpdate
protected void onUpdate() {
setUpdated(new Date());}

I soon realized an important drawback to this approach.  It simply doesn't work when using Hibernate's Session API.  This was a big problem for me since as I've discussed in detail in previous blog posts I am in fact using org.hibernate.Session.  :( As described in this Stack Overflow post, it is necessary to use the EntityManager API when using the @Prepersist and @PreUpdate annotations.

I considered converting from the Session API to the EntityManager API but decided against it for the time being.  That's when I looked more closely at Hibernate's interceptor and events architecture.  I also found a very helpful blog post by a blogger name neildo that explained how to use an event listener to manage last modified date.

Using neildo's blog post and the documentation as a starting point, I developed the following code that I am using today.  My code differs from the neildo's example in that I'm setting both the created and the modified dates in the custom event listener.

First there's the interface that each entity POJO must implement.  As you can see the interface includes "id" which I'll explain later.
public interface AuditableDate {

    public void setId(int id);
    public int getId();
    
    public void setCreated(Date date);
    public Date getCreated();
    
    public void setUpdated(Date date);
    public Date getUpdated();
}

Here's an example of an entity POJO that implements AuditableDate.  For this example, I've stripped out the rest of the entity properties and left only those needed to implement AuditableDate.  Obviously the real entity would have other properties as well.
public class Tag implements AuditableDate, Serializable {

    @Id
    @GeneratedValue
    @Column(name="TagId")
    @Override
    public int getId() {return this.id;}
    public void setId(int id) {this.id = id;}
    private int id;

    @Override
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="CreatedDate",
            nullable=false, 
        updatable = false)
    public Date getCreated() {return created;}
    public void setCreated(Date date) {this.created = date;}
    private Date created;
    
    @Override
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="UpdatedDate", nullable = false)
    public Date getUpdated() {return updated;}
    public void setUpdated(Date date) {this.updated = date;}
    private Date updated;

    public Tag() {
    }
}

The event listener is as follows.  You can see where I'm checking the Id property to determine if the we are working with a new record and if so I'm setting the both the Created Date and the Updated Date.
public class SaveOrUpdateDateListener extends DefaultSaveOrUpdateEventListener {
    
    static Logger logger = Logger.getLogger(SaveOrUpdateDateListener.class);

    @Override
    public void onSaveOrUpdate(SaveOrUpdateEvent event) {
        logger.debug("Entering onSaveOrUpdate()");
        
        if (event.getObject() instanceof AuditableDate) {
            AuditableDate record = (AuditableDate) event.getObject();
            
            // set the Updated date/time
            record.setUpdated(new Date());

            // set the Created date/time
            if (record.getId() == 0) {
                record.setCreated(new Date());
            }
        }
}

The last step is to add a reference to the Hibernate configuration so that it knows to fire the custom listener.
<event type="save-update">
    <listener class="org.robbins.persistence.SaveOrUpdateDateListener"/>
    <listener class="org.hibernate.event.def.DefaultSaveOrUpdateEventListener"/>
</event>

You'll notice that I've included BOTH the custom listener and the default listener.  I'm not sure why that was necessary but I was unable to get the default listener to fire in the custom listener by using:
super.onSaveOrUpdate(event);

I hope you found this post useful.  I'd appreciate any questions or suggestions you may have.  Thanks!

5 comments:

  1. u cud have used hibernate interceptor.

    ReplyDelete
  2. Great man. But you are specifying this only at model class level.
    Through Hibernate SPI Integrator you can listen at any model class

    ReplyDelete
  3. Is there any reason why you cant check if createdDate is null, instead of checking for id being 0 to detect a new entity? Thanks!

    ReplyDelete
  4. Is there a way to include createdby from user session?

    ReplyDelete