Currently Being Moderated

Our use of the OpenJPA APIs has typically been limited to the methods of the JPA EntityManager, which we wrap with our persistence engine class, JpaPersistenceEngineImpl.

We also make limited use of the OpenJPAQuery, EntityTransaction and FetchPlan classes.

However, OpenJPA provides some other very useful APIs, which can be used to inspect and/or control the way we load and persist our objects. Classes of particular interest include:

 

PCEnhancerUsed to enhance persistent classes from the metadata.
PCRegistryTracks registered persistence-capable classes.
ReflectionReflection utilities used to support and augment enhancement. Provides access to the fields and getters/setters of a persistent class.
OpenJPAStateManagerEach state manager manages the state of a single persistence capable instance. This can be used to inspect field dirtiness, access low-level fetch and store methods, and much more.
BrokerThe broker is the primary interface into the OpenJPA runtime. This can be used to add listeners for lifecycle-related events.
ClassMetaDataProvides information about the metadata of a class.
FieldMetaDataProvides information about the metadata of a field.
JPAFacadeHelperHelper class for switching between OpenJPA's JPA facade and the underlying Broker kernel. Useful for getting access to the metadata and the broker.
OpenJPAPersistenceHelper methods, including methods for casting to the JPA facades for the entity manager and entity manager factory.

 

In this post, we'll see first-hand the usefulness of these APIs by building an enhanced version of the persistence engine - one that logs JPA loads and saves.

Determining the dirty state of an object

The OpenJPA entity manager can be used to determine the dirtiness of an object as follows:

protected void logDirtyState(final Persistence object) {
  OpenJPAEntityManager openjpaEM = OpenJPAPersistence.cast(getEntityManager());
  if (openjpaEM.isDirty(object)) {
    LOG.info(object.getClass().getName() + " (" + object.getUidPk() + ") is dirty");
  }
}

 

Note that this only works for PersistenceCapable objects, i.e. classes that have been enhanced by OpenJPA. So it can't be used to determine the dirty status of every object that may be involved in a save or merge. For example, if an object has a String field that has been changed, calling isDirty on that field will return false because String is not an enhanced class.

However, you can find out exactly which fields of a PersistenceCapable object are dirty as follows:

protected void logDirtyFields(final PersistenceCapable pcObject) {
  OpenJPAStateManager manager = (OpenJPAStateManager) pcObject.pcGetStateManager();
  ClassMetaData metaData = JPAFacadeHelper.getMetaData(getEntityManager(), pcObject.getClass());
  BitSet dirtySet = manager.getDirty();
  for (int dirtyIndex = dirtySet.nextSetBit(0); dirtyIndex >= 0; dirtyIndex = dirtySet.nextSetBit(dirtyIndex + 1)) {
    FieldMetaData fieldMetaData = metaData.getField(dirtyIndex);
    LOG.info(pcObject.getClass().getName() + " has a dirty field: " + fieldMetaData.getName());
  }
}

 

The state manager's getDirty() method will give us a BitSet indicating the indexes of the fields that are marked as dirty. We use the JPAFacadeHelper to get the metadata for the class, and then we can get the metadata for each of the dirty fields.

Examining the dirty state of an entire object graph

The above demonstrated how to determine whether an object is dirty, and how to find out which fields of that object are dirty. But what if some of those fields are themselves PersistenceCapable objects or collections of objects?

Well, OpenJPA makes it relatively easy to traverse the object graph. Let's change the above code to do a bit more with the dirty fields:

for (int dirtyIndex = dirtySet.nextSetBit(0); dirtyIndex >= 0; dirtyIndex = dirtySet.nextSetBit(dirtyIndex + 1)) {
  FieldMetaData fieldMetaData = metaData.getField(dirtyIndex);
  Method method = Reflection.findGetter(pcObject.getClass(), fieldMetaData.getName(), true);
  logDirtyField(fieldMetaData.getName(), Reflection.get(pcObject, method), (Persistence) pcObject);
}

 

The Reflection class gives us access to the getter for the given field and thus we can traverse further into that field's object, as follows:

protected void logDirtyField(final String fieldName, final Object object, final Persistence parent) {
  if (object instanceof Collection) {
    for (Object member : (Collection<Object>) object) {
      logDirtyField(fieldName + " (member)", member, parent);
    }
    return;
  }
  if (object instanceof Map) {
    for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) object).entrySet()) {
      logDirtyField(fieldName + " (" + entry.getKey().toString() + ")", entry.getValue(), parent);
    }
    return;
  } 
  if (object instanceof PersistenceCapable) {
    logDirtyFields((PersistenceCapable) object);
  } else {
    LOG.info(parent.getClass().getName() + " (" + parent.getUidPk() + ") has dirty field " + fieldName 
                         + " with value " + object.toString());
}

 

As you can see, if we call this on a field that is a collection or map, it will just iterate through the members and pass each resultant member object into the logging method. If the field (or member) is itself a PersistenceCapable object, we call the original method to examine that object's dirty fields. Finally, when the field (or member) is not a collection, map, or PersistenceCapable object, we just log the fact it was found to be dirty.

To demonstrate this behaviour, if we edit a product and make some changes to its fields and attributes, we get a log similar to the below:

com.elasticpath.domain.attribute.impl.ProductAttributeValueImpl (169132) has dirty field shortTextValue with value Nice LCD display
com.elasticpath.domain.attribute.impl.ProductAttributeValueImpl (166706) has dirty field booleanValue with value false
com.elasticpath.domain.catalog.impl.ProductImpl (102808) has dirty field hidden with value true
com.elasticpath.domain.catalog.impl.ProductImpl (102808) has dirty field lastModifiedDate with value Mon Dec 08 18:10:42 PST 2008
com.elasticpath.domain.catalog.impl.ProductLocaleDependantFieldsImpl (105692) has dirty field displayName with value Simon's Kodak EasyShare C310...

Examining the object graph loaded by JPA

When we ask JPA to load an object, we often get more than we bargained for. What gets loaded depends on the relationships the object has to other objects, the annotated FetchType, and any load tuners or fetch groups defined. More often than not we get a sizeable object graph.

OpenJPA provides a convenient way to examine what gets loaded without having to use something like reflection to manually traverse the object graph. Instead, you can use define a LoadListener - just one of the many listeners for lifecycle-related events that JPA allows you to add.

We define our logging load listener as follows:

public class LoggingLoadListener implements LoadListener {
 
  public void afterLoad(final LifecycleEvent event) {
    Persistence sourceObject = (Persistence) event.getSource();
    LOG.log(logLevel, "Loaded " + sourceObject.getClass().getName() + " with UIDPK " + sourceObject.getUidPk());
  }
 
  public void afterRefresh(final LifecycleEvent event) {
    // Do nothing - we don't care about refresh events.
  }
 
}

 

The lifecycle methods get called with a LifecycleEvent object which provides the event's source object.

Note: If you want to listen to multiple kinds of JPA events, you consider subclassing OpenJPA's AbstractLifecycleListener which implements all the lifecycle listeners and delegates them to a single method eventOccurred(LifecycleEvent event).

To tell OpenJPA to call this listener whenever it loads, we use the JPAFacadeHelper to get a hold of the underlying Broker as follows:

private void addLoadListener() {
    OpenJPAEntityManager openjpaEM = OpenJPAPersistence.cast(getEntityManager());
    Broker broker = JPAFacadeHelper.toBroker(openjpaEM);
    broker.addLifecycleListener(new LoggingLoadListener(), null);
}

A logging persistence engine

Now we know how to get this information, how do we seamlessly integrate it into our Elastic Path application? The easiest way is to create a subclass of the main Elastic Path JPA persistence engine, JpaPersistenceEngineImpl. We then override the persistence methods to call our logging functions.

For example:

public class LoggingPersistenceEngineImpl extends JpaPersistenceEngineImpl {
 
  @Override
  public <T extends Persistence> T merge(final T object) throws EpPersistenceException {
    logDirtyState(object);
    return super.merge(object);
  }
 
  @Override
  public void save(final Persistence object) throws EpPersistenceException {
    logDirtyState(object);
    super.save(object);
  }
 
  @Override
  public <T extends Persistence> T get(final Class<T> persistenceClass, final long uidPk) throws EpPersistenceException {
    addLoadListener();
    return super.get(persistenceClass, uidPk);
  }

 

...and similarly for the other methods (retrieve, retrieveByNamedQuery, etc.).

Then we just change the Spring wiring to use our new logging persistence engine as follows (found in conf/spring/dataaccess/openjpa/openjpa.xml)

<bean id="persistenceEngineTarget"
      class="com.elasticpath.persistence.impl.LoggingPersistenceEngineImpl">
  <property name="entityManager" ref="sharedEntityManager"/>
  <property name="sessionFactory" ref="sessionFactory"/>
</bean>

Enhancement - limiting the list of classes to be logged

There may be classes you want to exclude from the logging - such as search index related classes that may result in too much log noise. One easy way of being able to configure the set of classes you want logged is just to define the list through Spring. For example, we add the following property to the persistence engine bean definition:

<property name="loggingClasses">
  <set>
    <value>com.elasticpath.domain.attribute.impl.AbstractAttributeValueImpl</value>
    <value>com.elasticpath.domain.attribute.impl.AttributeGroupAttributeImpl</value>
    <value>com.elasticpath.domain.attribute.impl.AttributeImpl</value>
    <value>com.elasticpath.domain.attribute.impl.ProductAttributeValueImpl</value>
    <value>com.elasticpath.domain.attribute.impl.ProductTypeProductAttributeImpl</value>
    <value>com.elasticpath.domain.catalog.impl.AbstractPriceImpl</value>
    <value>com.elasticpath.domain.catalog.impl.AbstractPriceTierImpl</value>
    <value>com.elasticpath.domain.catalog.impl.CatalogProductPriceImpl</value>
    <value>com.elasticpath.domain.catalog.impl.AbstractLocaleDependantFieldsImpl</value>
    <value>com.elasticpath.domain.catalog.impl.ProductAssociationImpl</value>
    <value>com.elasticpath.domain.catalog.impl.ProductImpl</value>
    <value>com.elasticpath.domain.catalog.impl.ProductLocaleDependantFieldsImpl</value>
    <value>com.elasticpath.domain.catalog.impl.ProductPriceImpl</value>
    <value>com.elasticpath.domain.catalog.impl.ProductPriceTierImpl</value>
    <value>com.elasticpath.domain.catalog.impl.ProductTypeImpl</value>
  </set>
</property>

 

Then we add the relevant property, getter and setter to our class:

private Collection<String> loggingClasses;
 
public Collection<String> getLoggingClasses() {
  return loggingClasses;
}
 
public void setLoggingClasses(final Collection<String> loggingClasses) {
  this.loggingClasses = loggingClasses;
}

 

Finally, we add the following to our logDirtyState and afterLoad methods:

if (!getLoggingClasses().contains(object.getClass().getName())) {
  return;
}

Enhancement - configurable log level

Suppose we want to allow the log level used by our logging persistence engine to be configurable through spring. We can just add a getter and setter as follows:

private Level logLevel;
 
  public String getLogLevel() {
    return logLevel.toString();
  }
 
  public void setLogLevel(final String logLevel) {
    this.logLevel = Level.toLevel(logLevel);
  }

 

Then we just use the log4j log(Priority priority, Object message) method whenever we log. Make sure to set this in spring by adding the property to your bean definition:

<property name="logLevel" value="INFO"/>



Tags: openjpa