Skip to main content

Integrating EclipseLink with EhCache to cache ReadAll and Native Queries

Posted by zarar on June 1, 2010 at 9:18 AM PDT

The problem at hand is that EclipseLink (great project lead by James Sutherland) does not use a query cache when dealing with ReadAll queries, i.e: all calls to getResultList() go to the database.  Some object-level caching is performed by avoiding construction of new objects based on the primary key values the database call returns.  EclipseLink compares the PK values returned by the getResultList() query to that in its identity cache and if matches are found, the cached objects are returned.  After running JProfiler, I determined that the saving weren't really significant at all as the query was being executed every time and only entity creation was avoided.  

I realize that caching calls from getResultList() can be dangerous as changes from other applications will not be reflected, and that is the argument I received on the mailing list.  The argument carries merit, but since the default behavior of EclipseLink is to maintain an identity cache as described in the previous paragraph, any outside changes to non-primary key values will not be reflected anyway, so I don't see what the big deal about caching ReadAll queries really is. EclipseLink can't cache native queries since they aren't mapped nicely to JPA @Entity objects.  Since I'm working on a legacy system with a database design that resembles spaghetti and meatballs which also happens to be heavily used, I desperately needed to cache some queries that I know return fairly static data.

Anyway, so I went about trying to implement a cache and thankfully, EclipseLink provides a great extension point through query redirectors.  Basically, specifying a QueryRedirector on a query will allow you to control the logic right before query execution.   The @QueryRedirectors annotation has a bug which forces you to specify all attributes so I went for a cleaner, programmatic solution.  

First, we must specify the a redirector for the query created using the EntityManager:

Query query = em.createQuery("getOrdersByCustomerLastName").setParameter("lastName", "Smith");
JpaHelper.getDatabaseQuery(query).setRedirector(new AggressiveCacheQueryRedirector());

EclipseLink's  JpaHelper.getDatabaseQuery() helper method provides access to the EclipseLink-specific DatabaseQuery object which contains the setRedirector() method.  The AggressiveCacheQueryRedirector class must implement the QueryRedirector interface.  Here's the complete implementation of the redirector:

 

public class AggressiveCacheQueryRedirector implements QueryRedirector {

    /**
     * Returns a cached result if applicable, otherwise invokes query
     * @param query query to execute
     * @param arguments arguments
     * @param session session
     * @return cached result or query results
     */
    public Object invokeQuery(DatabaseQuery query, Record arguments, Session session) {

        // In order to avoid loop, stop redirecting once execution finishes
        query.setDoNotRedirect(true);

        // Caching read-all queries
        if (query.isReadAllQuery()) {

            // Find or create the cache based on class descriptor
            EhCacheWrapper cacheWrapper = EhCacheWrapper.getInstance();
            Cache cache = cacheWrapper.findOrCreateCache(query.getDescriptor());

            // Create a key for the cache and query it
            CacheKey key = new CacheKey(query.getName(), arguments.values().toArray());
            Element element;

            // Return cached result
            if ((element = cache.get(key)) != null) {
                return element.getValue();

            // Execute query and store result in cache
            } else {
                Object object = query.execute((AbstractSession)session, (AbstractRecord) arguments);
                if (object != null) {
                    cache.put(new Element(key, object));
                }
                return object;
            }
         // Execute query as-is, no caching is done
        } else {
            return query.execute((AbstractSession)session, (AbstractRecord) arguments);
        }
    }
}

It's fairly simple, all it's doing is checking EhCache before deciding whether to execute the query. The query name and argument values are the keys of the cache, with the values being the results returned by the query upon execution. The EhCacheWrapper class is just that, a wrapper for EhCache specific functions. A plain implementation is given below. Of note is the @AggressiveCache annotation which I created to denote entities that need to be cached using EhCache. It has two attributes, the cache duration in seconds and the size of the cache, corresponding to the setExpiryInSeconds and setMaxElementsInMemory of the EhCache's Cache object.

public class EhCacheWrapper {

    private static EhCacheWrapper ehCacheWrapper;

    /**
     * Static create method
     * @return The singleton instance of EhCacheWrapper
     */
    public static EhCacheWrapper getInstance() {
        if (ehCacheWrapper == null) {
            ehCacheWrapper = new EhCacheWrapper();
        }
        return ehCacheWrapper;
    }


    /** CacheManager singleton instance **/
    private CacheManager singletonManager;

    /**
     * Private constructor which initializes CacheManager
     */
    private EhCacheWrapper() {
        try {
            singletonManager = new CacheManager(new ClassPathResource("ehcache.xml").getURL());
        } catch (IOException e) {
            throw new RuntimeException();
        }
    }


    /**
     * Returns a cache if one exists, otherwise creates one
     * @param descriptor EclipseLink class descriptor
     * @return A newly created on pre-existing Cache
     */
    public Cache findOrCreateCache(ClassDescriptor descriptor) {
        String cacheName = descriptor.getJavaClass().getSimpleName();
        Cache cache = singletonManager.cacheExists(cacheName) ? singletonManager.getCache(cacheName) : null;
        if (cache == null) {
            Cache newCache = createCache(descriptor);
            singletonManager.addCache(newCache);
            return newCache;
        } else {
            return cache;
        }
    }

    /**
     * Creates a cache either by calling createCacheFromCacheAnnotation() or createDefaultCache()
     * @param descriptor EclipseLink class descriptor
     * @return A newly created Cache
     */
    @SuppressWarnings("unchecked")
    private Cache createCache(ClassDescriptor descriptor) {
        AggressiveCache annotation = (AggressiveCache) descriptor.getJavaClass().getAnnotation(AggressiveCache.class);
        CacheConfiguration config = new CacheConfiguration();
        config.setStatistics(true);
        config.setMaxElementsInMemory(annotation.size());
        config.setTimeToLiveSeconds(annotation.expiryInSeconds());
        config.setEternal(false);
        config.overflowToDisk(false);
        config.setName(descriptor.getJavaClass().getSimpleName());
        return new Cache(config);
    }
}

Here's the @AggressiveCache annotation.

public @interface AggressiveCache {

    /**
     * Corresponds to EhCache's Cache.setMaxElementsInMemory
     * @return EhCache's Cache.setMaxElementsInMemory
     */
    int size() default 1000;

    /**
     * Corresponds to EhCache's Cache.setTimeToLiveSeconds
     * @return EhCache's Cache.setTimeToLiveSeconds
     */
    int expiryInSeconds() default 60000;

}

For completeness, here's the CacheKey class which stores the query name and the argument values.

public class CacheKey {

    private String queryName;
    private Object[] arguments;

    /**
     * Store the query name and its argument as the key
     * @param queryName Name of query
     * @param arguments Arguments of query
     */
    public CacheKey(String queryName, Object[] arguments) {
        this.queryName = queryName;
        this.arguments = arguments;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        CacheKey cacheKey = (CacheKey) o;

        if (!Arrays.equals(arguments, cacheKey.arguments)) return false;
        if (queryName != null ? !queryName.equals(cacheKey.queryName) : cacheKey.queryName != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = queryName != null ? queryName.hashCode() : 0;
        result = 31 * result + (arguments != null ? Arrays.hashCode(arguments) : 0);
        return result;
    }
}
Related Topics >>