Pizza Shop III : JPA Event Listeners

2008-03-31

This comment was recently posted to one of my blog entries a while back:

Candies with 'Yay Audits!' written on them

So, let’s say your data model has some tables with a columns that indicate the last person who touched a record, like Madhan’s example above. In most applications, the end-users of the database client share a common login to the database, and have individual logins which are specific to the application’s domain (i.e., you don’t have a database login mapped to each end user; maintaining this scheme would be a nightmare). For that reason, triggers can’t be used as a solution, because database triggers don’t know anything about which application end-user is responsible for making a data change.

The last thing you want to do is litter your codebase with snippets of code that set the username on your persisted objects manually; not only is it unnecessary duplication, but you’ll probably also end up missing cases where you should be setting the username.

EventListeners to the Rescue
Fortunately, JPA supports the notion of “EventListeners.” An event listener intercepts many of the API calls that modify a persisted object’s lifecycle, and thus may be used to inject business logic that needs to be duplicated over many different objects. AOP aficionados might refer to this as a cross-cutting “aspect” of the domain layer.

Return to the Pizza Shop
Readers of my blog (all three of them, including me) might recall my venerable Pizza Shop example. Here are the earlier Pizza Shop posts if you’d like to catch up: Part 1 and Part 2. I’m going to drag the Pizza Shop out again to demonstrate how to create an “audited” table, which shows the last user who modified a record. As always, the source is available here, and it’s been tested against MySQL, Postgres, and MS SQL Server, with Hibernate, OpenJPA, and TopLink.

This takes just five easy steps… four, really, ‘cuz the fourth step creates a mock object for testing, so it doesn’t really count!

Step One – New schema
Add a nullable column named username to the Order table… something like this should work if you have existing pizzashop schema for some reason:

ALTER TABLE ORDER ADD username VARCHAR(10)

Step Two – Create an Interface and Implement It
This step isn’t strictly necessary, but it’s probably safe to assume that we’ll want to add this functionality to other tables someday. The interface just adds accessor methods for the username:

public interface AuditedObject {
  public String getUsername();
  public void setUsername(String username);
}

Then, make the Order table implement AuditedObject. Add a member variable with a mapping to the username column to the Order table, and corresponding accessor methods:

public class Order implements IdObject, AuditedObject {
.
.
  @Basic @Column(name="username")
  private String username;
.
.
  public String getUsername() {
      return username;
  }
  public void setUsername(String username) {
      this.username = username;
  }
.
.
}

Step Three – Add an EntityListener Annotation
This is the secret sauce. In the JPA Framework, EventListeners allow you to fire some trigger code when a lifecycle event occurs on a persisted object. You do this by associating your persisted class with an EventListener class. We’ll implement our EventListener class after we’ve added the following annotations:

@Entity @Table(name="PIZZA_ORDER")
@EntityListeners(AuditedEntityListener.class)
public class Order implements IdObject, AuditedObject {
.
.

Step Four – Write a Mock Object for the Username for Testing
In the real world, you’d probably stuff the username into the servlet context object. For our simple tests, we’ll need to mock up an object to maintain a username for the duration of our tests. It can be something stupidly simple, like this:

public class MockContext {
  private static String username;
 
  public static String getUsername() {
      return username;
  }
  public static void setUsername(String username) {
      MockContext.username = username;
  }
}

We’ll set the username at the start of our tests, and read the username from our MockContext from the EventListener.

Step Five – Write the EntityListener class
JPA allows you to inercept method calls using seven annotations: * @PrePersist and @PostPersist are called before and after an object is persisted. * @PreUpdate and @PostUpdate are called before and after synchronization with the database. @PreRemove and @PostRemove are called before and after an object is removed from the persistent state. * @PostLoad is invoked immediately after an object is loaded from the database.

In our case, we’re going to populate the username instance variable when the object is persisted, so we will want to create an EntityListener class with the @PrePersist annotation. The method’s signature takes an Object parameter which is the object getting updated, and returns void. The class will look something like this:

public class AuditedEntityListener {
  @PrePersist
  public void updateUser(Object o) {
      if (o instanceof AuditedObject) {
        String username = MockContext.getUsername();
        ((AuditedObject)o).setUsername(username);
      }
  }
}

Voila! When you run the tests, the username fields are populated and persisted!

Audit M&M’s Photo Courtesy Joe Hall.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.