Technical Blog

4 Posts authored by: Simon Droscher

In Star Trek there is a test named the Kobayashi Maru given to all Starfleet Academy cadets. In this test the civilian vessel Kobayashi Maru is disabled and trapped in the Klingon Neutral Zone, and the cadet must choose whether to leave the ship to certain destruction or to attempt a rescue but risk the lives of their own ship and crew and possibly provoke the Klingons into an all-out war. If the cadet chooses to attempt a rescue the simulation inevitably ends in battle and complete destruction of the cadet's ship. The test is designed as a no-win situation to test the cadet's character.

 

How is this relevant to OpenJPA and EP you ask? In our case the Kobayashi Maru is your e-commerce store - stuck in the neutral zone between development and go live, disabled due to performance issues caused by a large object graph. The cadet is you, the developers. The Klingons represent the project stakeholders who will fight you to meet deadlines whilst requiring quality maintainable and functional code. What makes it a no-win situation is there is no simple way to tell OpenJPA to load just specific parts of the object graph. You have the following options:

 

  • Try to use the EP load tuners to flag which parts of the object graph you are not interested in.
    • For example, the ProductLoadTuner lets you specify setLoadingCategories(false) to avoid loading all of the categories that a product belongs to. Seems sound, right? The only problem is it doesn't work! For legacy reasons there are many service methods and queries that load a product (either directly or as part of another object graph) and expect the categories - so a long time ago the JPA annotations on ProductImpl.getProductCategories() was set to FetchType.EAGER. This means JPA will always load the categories. If you change the annotation to use FetchType.LAZY then you would need to find everywhere in the system that loads a product expecting to see categories and change it to use the appropriate load tuner. You don't have that much time, and so you are defeated by the Klingons.
  • Create a new OpenJPA FetchGroup which is a way of defining exactly which fields to load.
    • This is the strategy that has already been used in parts of the code - for example when building the product index. The drawbacks of this approach is that a fetch group will only load the specified fields, and you can only define a fetch group through annotations in the same classes that the fields are defined in. If you are trying to use Binary Based Development then this option is already disqualified. If you are happy to make core changes, then you need to edit every class in the object graph to define the fields that should be loaded as part of your fetch group. A quick search shows the PRODUCT_INDEX fetch group is defined across 20 classes! Miss one field in one class and you get an unexpected NullPointerException. You don't have time to do all the testing required to ensure you have all the necessary fields, and so you are either defeated by the Klingons or your ship implodes later due to a null pointer in the warp core.
  • Change the OpenJPAFetchPlanHelperImpl to remove the default fetch group.
    • This is a special fetch group created by OpenJPA (FetchGroup.NAME_DEFAULT) which includes all basic fields, *ToOne fields, and other fields marked with FetchType.EAGER. Remove this and you then need to add back all the fields that you want to load. So you are most likely in the same situation as above. Even if you try to do something clever like having a spring injected list of fields to include, this will need to be maintained in the future when new fields are added to any of the classes involved, and you still need to do exhaustive testing to ensure no essential field was missed. So the Klingons win again.

 

 

I am now going to play the role of Captain James Kirk. Kirk stated "I don't believe in the no-win scenario", and his response to the Kobayashi Maru test was to subtely reprogram the simulator to make it possible to rescue the vessel. I'm going to reprogram our simulator using some of the OpenJPA API.

 

We can use the OpenJPA metadata API to recreate the "default" fetch plan in such a way as to allow ad-hoc addition and removal from this plan. Here's a code snippet that does this:

 

protected Set<String> getDefaultFetchFields(final Class< ? > persistenceClass, final EntityManager entityManager, final Set<Class<?>> configuredObjects) {
     Set<String> fetchFields = new HashSet<String>();
 
     if (configuredObjects == null) {
          configuredObjects = new HashSet<Class<?>>();
     } else if (configuredObjects.contains(persistenceClass)) {
          return fetchFields;
     }
     configuredObjects.add(persistenceClass);
 
     ClassMetaData classMetaData = JPAFacadeHelper.getMetaData(entityManager, persistenceClass);
     FieldMetaData[] fields = classMetaData.getFields();
     for (FieldMetaData field : fields) {
          if (field.isInDefaultFetchGroup()) {
               fetchFields.add(field.getFullName(false));
               if (field.isTypePC()) {
                    fetchFields.addAll(getDefaultFetchFields(field.getType(), entityManager, configuredObjects));
               } else if (field.getElement() != null && field.getElement().isTypePC()) {
                    getDefaultFetchFields(field.getElement().getType(), entityManager, configuredObjects);
               }
          }
     }
 
     return fetchFields;
}

 

The first block keeps track of any classes we have already processed since there can be self-references or bi-directional relationships in an object graph. We then use OpenJPA's JPAFacadeHelper class to get the metadata for the given class. This provides a collection of persistence fields - both in the current class and any of its superclasses. We loop through these fields and examine each one in turn. The field metadata will tell us if the field is in the default fetch group, and if so we add it to the list of fields to include. Next, we check if the field is itself a PersistenceCapable class, in which case we recurse down into the fields of that class, and so on. For a *ToMany relationship the field will have an element which may be PersistenceCapable so we recurse into that similarly.

 

The results of all this is a list of (fully qualified) field names that are in the default fetch group for the entire object graph of the given class. We can then tweak this list, removing any fields we don't want loaded and adding other (non-default) fields we specifically want to load. We can then tell OpenJPA to remove the default fetch group, and give it our list as the definitive list of fields to load. For example:

 

Set<String> productFetchFields = getDefaultFetchFields(beanFactory.getBeanImplClass(ContextIdNames.PRODUCT), entityManager, null);
fetchPlan.removeFetchGroup(FetchGroup.NAME_DEFAULT);
fetchPlan.addFields(productFetchFields);
fetchPlan.removeField(ProductImpl.class, "productCategories"); 

 

We now have a fetch plan that tells OpenJPA to load a product without the categories!

 

Using this technique you can easily define to a fine degree exactly what gets loaded in your object graph and thus rescue the civilian vessel without arousing the wrath of the Klingons.

 

In my next mission as captain of this starship I will refactor the OpenJPAFetchPlanHelperImpl to use this technique which will enable the load tuners flags to actually work as expected!  I'll also add methods to that class (and interface) to allow easy removal of fields and also a method for removal of all fields (except the uidPk) of a class. Sometimes having that object "reference" on the object graph is sufficient without requiring any of the other fields, and it would be tiresome to remove the fields from the plan individually. For an encore I will replace all the hard-coded @FetchGroup annotations with a spring-injectable plan which is more extension-friendly (whilst still preserving current behaviour).

 

Feeback welcome, in the meantime Live Long and Prosper!

10 Comments Permalink

Spike for Success

Posted by Simon Droscher Sep 20, 2010

We've all faced the situation where the team needs to implement a story that no-one knows enough about to be able to estimate well. Estimating the story will be based on speculation rather than concrete data, and the story has the potential to go horribly wrong and remain at "almost done" for the entire iteration (and beyond).

 

Why does it go wrong so easily? Well the short answer is that the team will be making their best effort to develop the functionality for the story and resolve issues as they arise. This often means having to go back and rewrite code written earlier in the iteration as the problem is better understood. There may be several failed attempts before everything is understood and all problems are solved.

 

There's a better way to make this kind of story successful - something we know as a Spike.

 

The idea of a Spike is this - spend a fixed amount of time (also known as timeboxing) focused on the problem and ignoring all other concerns. These "concerns" that we can ignore as part of a spike include such things as code compliance, design principles, best practices etc. What we want to do is consider some approaches and do the minimum amount of work required to demonstrate whether the approach is feasible or not. We are experimenting with ideas on how to possibly implement the story, rather than trying to complete the story as a whole. Keeping it time-boxed allows us to get back to delivering tangible value rather than taking an extended journey down the rabbit hole.

 

At the end of the spike, we should have reduced the risk involved in implementing the story - we now have concrete data rather than speculation. Now we can estimate the work required to implement the story with a lot more confidence!

 

Typically, the deliverables of a spike should include a writeup that outlines one or more approaches, along with some working code that demonstrates the approach(es). It may go as far as producing a design for peer review. Generally the code produced by a spike is considered throw away, or at best a starting point for the implementation. It demonstrates the approach without being Production-ready code. A good thing to include in the writeup is what work will be required to take the concept to production-ready code. Other developers should be able to read the writeup and understand enough about the story to be able to estimate with confidence.

 

One rule we generally stick to in order to ensure the appropriate production-code standards are followed in the actual implementation is to ban the developer who worked on the spike from developing the actual implementation story. In a self-organizing team this also ensures (via peer reviews) that the writeup is sufficiently detailed to allow anyone to implement the production story.

 

 

Some examples of successful spikes we have done at Elastic Path:

 

1. Faceted Navigation for Price List Stack

 

The aim of this spike, during 6.2.0 development, was to demonstrate ways to ensure price faceting works in the storefront now that prices are determined by a changeable stack of price lists rather than being directly on the Product object. We started by brainstorming possible approaches, and narrowed it down to 2 that we would like to try:

 

  • Fields in the indexed document for each possible combination of price lists in a stack (may limit number of price lists/catalog)
  • Field in the document per price list, and facet on a query given the price lists involved in the stack. Care needs to be taken to exclude results already in another facet.

 

Both approaches were tried and working code was produced for each. We were then able to weigh up the pros and cons of each and decide the ultimate approach. We decided on the second approach due to the fact that there was negligible difference in query time for each, but the first solution had product indexing times increasing dramatically as stores and price lists were added.

 

Using this spike, we were able to implement the story - which originally made a whole development team very nervous - with a minimum of fuss.

 

2. Upgrading to Drools 5

 

During 6.2.1 development we received feedback from customer's desiring an upgrade of our supported version of Drools Rules from 3.0.4 to 5.0.1. Our Product Manager wanted to put this upgrade on the roadmap only if it would be small enough to not impact our ability to deliver other valuable features to the product. In a time-boxed spike we were able to establish that a small amount of work would be required due to some Drools API changes but that were no major roadblocks.

 

3. (Not) Upgrading to OpenJPA 1.2.2

 

During 6.2.1 development we did a spike to determine the feasibility of upgrading from OpenJPA 1.2.1 to 1.2.2. We were able to establish that upgrading to 1.2.2 would require a significant amount of work as there were still many patches required for it to work with the EP codebase, and no bug fixes or no features that would benefit us.

0 Comments Permalink

When developing your ecommerce site using Elastic Path, using the Eclipse WTP functionality to run your web applications from within Eclipse allows easy debugging. However, there are a few common problems that can occur which can disrupt your development and have you cursing Eclipse. Here are the top 5:

 

1. Not having enough memory allocated

By default, Java allocates only 128 Mb of memory for the heap and 64 Mb for the perm size. This isn't nearly enough for large applications. If you haven't told you server to allocate sufficient memory, it may start up without error, but once you try to do anything significant it will crash with an error like the following:

Exception in thread "RMI TCP Connection(idle)" java.lang.OutOfMemoryError: PermGen space

or

java.lang.OutOfMemoryError: Java heap space

 

As per the developer documentation, ensure you have enough heap and perm space by double-clicking your server in the Servers view, clicking Open launch configuration and adding the following to the VM arguments:

-Xmx1024m -Xms256m -XX:MaxPermSize=512m

 

If you are using Java 6 you should also ensure you have the following:

-Dsun.lang.ClassLoader.allowArraySyntax=true

 

2. Classes not enhanced

If you make changes to core persistence classes in Eclipse, it may compile the class without running the OpenJPA enhancement. When you start your application in WTP, it will try to do runtime enhancement and fail with an error like the following:

java.lang.ClassNotFoundException: org.apache.renamed.openjpa.enhance.InstrumentationFactory
Exception in thread "Attach Listener"      at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
     at java.security.AccessController.doPrivileged(Native Method)
     at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:315)
     at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:330)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:250)
     at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:280)
     at sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:348)
Agent failed to start!

 

You may also see an error like the following:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'messageSource' defined in ServletContext resource
[/WEB-INF/conf/spring/views/velocity/velocity.xml]: Cannot resolve reference to bean 'storeMessageSourceDelegate' while setting bean property 'storeMessageSource';
nested exception is org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'storeMessageSourceDelegate' defined in ServletContext resource [/WEB-INF/conf/spring/views/velocity/velocity.xml]:
Cannot resolve reference to bean 'messageSourceCache' while setting bean property 'messageSourceCache'; 
nested exception is org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'messageSourceCache' defined in ServletContext resource [/WEB-INF/conf/spring/views/velocity/velocity.xml]: 
Invocation of init method failed; 
nested exception is <openjpa-1.2.1-r62037:76497M nonfatal user error> org.apache.renamed.openjpa.persistence.ArgumentException:
[Error while processing persistent field com.elasticpath.domain.order.impl.AbstractOrderShipmentImpl.status, declared in null. Error details:
The accessor for field getStatus in type com.elasticpath.domain.order.impl.AbstractOrderShipmentImpl is private or package-visible. 
OpenJPA requires accessors in unenhanced instances to be public or protected. 
If you do not want to add such an accessor, you must run the OpenJPA enhancer after compilation, or deploy to an environment that supports deploy-time enhancement,
such as a Java EE 5 application server.]

 

To avoid these, ensure the classes are enhanced by doing one of the following whenever you change a core persistence class:

  • Run ant enhance from the command line if you are working directly on com.elasticpath.core. If you do this be sure to refresh the project in Eclipse.
  • Launch an ant-based enhance from within Eclipse by running coreAntJpaEnhance.launch in core
  • If using binary based development, ensure you are using the Maven OpenJPA plugin for enhancement and you have Eclipse configured to call the maven build

 

 

3. Missing datasource context

The datasource used by the Elastic Path applications to connect to the database is configured through JNDI. When using WTP with Tomcat, for example, you need to make sure you have the JNDI datasource defined in the container. If it can't be found, you will end up with an error something like the below (note that this will be preceded by several pages of  various "Error creating bean..." messages:

Caused by: org.apache.tomcat.dbcp.dbcp.SQLNestedException: Cannot create JDBC driver of class '' for connect URL 'null'
     at org.apache.tomcat.dbcp.dbcp.BasicDataSource.createDataSource(BasicDataSource.java:1150)
     at org.apache.tomcat.dbcp.dbcp.BasicDataSource.getConnection(BasicDataSource.java:880)
     at org.apache.renamed.openjpa.lib.jdbc.DelegatingDataSource.getConnection(DelegatingDataSource.java:106)
     at org.apache.renamed.openjpa.lib.jdbc.DecoratingDataSource.getConnection(DecoratingDataSource.java:87)
     at org.apache.renamed.openjpa.jdbc.sql.DBDictionaryFactory.newDBDictionary(DBDictionaryFactory.java:91)
     ... 115 more
Caused by: java.sql.SQLException: No suitable driver
     at java.sql.DriverManager.getDriver(DriverManager.java:264)
     at org.apache.tomcat.dbcp.dbcp.BasicDataSource.createDataSource(BasicDataSource.java:1143)
     ... 119 more

 

When using Tomcat with the Elastic Path web applications, the ant eclipse-setup task will create a META-INF/context.xml file which defines the datasource using values from your env.config. Ensure this file is present and has the database details you are expecting.

 

Alternatively (or when using binary based development), you may add a <Resource> definition to the Context for your web app directly in the server.xml file. In either case it will look as follows:

    <Resource name="jdbc/epjndi"
        auth="Container"
        scope="Shareable"
        type="javax.sql.DataSource"
        maxActive="100"
        maxIdle="30"
        maxWait="10000"
        removeAbandoned="true"
        username="root"
        password="password"
        driverClassName="com.mysql.jdbc.Driver"
        url="jdbc:mysql://localhost:3306/COMMERCE_DB?AutoReconnect=true&useUnicode=true&characterEncoding=utf-8"
    />

 

4. Changes not published

Another common problem is making changes in Eclipse and (mysteriously) not having these changes present when you start your web applications. One cause of is related to the fact that our ant build copies spring configuration to the web applications at build time. All the files from WEB-INF/conf/spring are copied, namely:

 

  • commons/serviceChangeset.xml
  • commons/util-config.xml
  • dataaccess/openjpa/openjpa.xml
  • dataaccess/dao.xml
  • models/domainModel.xml
  • service/service.xml
  • service/serviceRemote.xml

 

If you change any of these files in core within Eclipse, they won't get automatically copied across to your web applications; you will need to re-run ant build for those projects from the command line. Conversely, if you change the copies in your web applications directory, then next time you run ant build your changes will get overwritten.

 

You may also encounter issues where code changes you make do not seem to be present when running your web applications. If this occurs, you may want to check the following:

  • Did you already have your server running when you made changes? Don't rely on Eclipse's ability to "hot swap" code into place.
  • Do you have maven Workspace Resolution disabled? If so, WTP won't be looking at the eclipse-compiled version of your code, but rather using the last version that was installed in your local maven repository. If you do feel the need to have workspace resolution disabled, you will need to run the appropriate ant or maven target on the command line to build and install the changed code into the repository.
  • Did you make any changes outside of Eclipse (including updating from your source code repository if working in a team environment) or run a command line build? If so, make sure you refresh the project in Eclipse so it knows there are changes to publish to WTP

 

5. Unable to publish due to file locks

This one happens less frequently than the others but is one of the more frustrating problems. Sometimes when you have been stopping and starting your web applications through WTP enough, Eclipse may have held onto a file lock for a little to long, and you get a message popup during publishing/startup that it is unable to publish a particular file. It is quite tempting to treat the OK button of this popup message as a "yeah, whatever" button and just continue. Sometimes when this occurs everything does still work, but other times Eclipse will, from that point on, fail to start your web applications properly.

 

There is no specific error message, but rather one or more of the apps will fail to start up and you may see pages of meaningless tomcat DEBUG lines or blank lines. The only good solution to this is to shut down Eclipse completely and relaunch it, which forces it to release the file locks and thus allow a republish. You may also need to right-click your server in the Servers view and select the Clean options to force a full republish.

0 Comments Permalink

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"/>



0 Comments Permalink