Thursday 18 October 2012

Spring, Hibernate and multiple datasources. What about the cache?

I recently ran in to an issue where I needed to be able to select a datasource for use with Hibernate. After some searching on the web I found this Dynamic Datasource Routing by Mark Fisher. It turns out that selecting a datasource is actually quite simple. Just create a subclass from AbstractRoutingDataSource as described in the the blog post and you are able to select the datasource. 

This is a very elegant solution and it works like a charm, until.... you start using cache. Because the Hibernate session factory assumes you are using a single datasource it also assumes that the cache is for this datasource. Entities and collections stored by there classname and the entity id. This will result in cache collision between values from different datasources.

One of the comment suggests wrapping the cache in a wrapper class. But this is a problem in a versions of Hibernate where the CacheProvider is deprecated (3.3 and later). In my opinion a more elegant solution is to use AspectJ to define an aspect that intercepts the call to the EntityRegionAccessStrategy and CollectionRegionAccessStrategy interfaces. By substituting the CacheKey parameter by a my own DatabaseDependentCacheKey I am able to prevent potential cache collisions.

The aspect I used is fairly simple if you understand AspectJ. What is done in this aspect is that all interface methods where an CacheKey is passed are defined as pointcuts. Then I define multiple around advices where I wrap the original key in my DatabaseDependentCacheKey.
Since this happens for all action (insert, update, delete evict) no collisions can occur.
The aspect below is for the hibernate 3.6.9. In Hibernate 4 the interfaces have changed slightly.
package com.fennek.gem.datasource.lookup;

import org.aspectj.lang.JoinPoint;
import org.hibernate.cache.CacheKey;
import org.hibernate.cache.access.CollectionRegionAccessStrategy;
import org.hibernate.cache.access.EntityRegionAccessStrategy;
import org.hibernate.cache.access.SoftLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public aspect CacheKeyModifierAspect 
{
 private static final Logger log = LoggerFactory.getLogger( CacheKeyModifierAspect.class );
 
 pointcut afterInsertEntity(Object key): call(boolean EntityRegionAccessStrategy+.afterInsert(Object, ..)) && args(key, ..);
 pointcut afterUpdateEntity(Object key): call(boolean EntityRegionAccessStrategy+.afterUpdate(Object, ..)) && args(key, ..);
 pointcut evictEntity(Object key): call(void EntityRegionAccessStrategy+.evict(Object)) && args(key);
 pointcut getEntity(Object key): call(Object EntityRegionAccessStrategy+.get(Object, ..)) && args(key, ..);
 pointcut insertEntity(Object key): call(boolean EntityRegionAccessStrategy+.insert(Object, ..)) && args(key, ..);
 pointcut lockItemEntity(Object key): call(SoftLock EntityRegionAccessStrategy+.lockItem(Object, ..)) && args(key, ..);
 pointcut putFromLoadEntity(Object key): call(boolean EntityRegionAccessStrategy+.putFromLoad(Object, ..)) && args(key, ..);
 pointcut removeEntity(Object key): call(void EntityRegionAccessStrategy+.remove(Object)) &&  args(key);
 pointcut unlockItemEntity(Object key): call(void EntityRegionAccessStrategy+.unlockItem(Object, ..))  && args(key, ..); 
 pointcut updateEntity(Object key): call(boolean EntityRegionAccessStrategy+.update(Object, ..)) && args(key, ..);

 pointcut evictCollection(Object key): call(void CollectionRegionAccessStrategy+.evict(Object)) && args(key);
 pointcut getCollection(Object key): call(Object CollectionRegionAccessStrategy+.get(Object, ..)) && args(key, ..);
 pointcut lockItemCollection(Object key): call(SoftLock CollectionRegionAccessStrategy+.lockItem(Object, ..)) && args(key, ..);
 pointcut putFromLoadCollection(Object key): call(boolean CollectionRegionAccessStrategy+.putFromLoad(Object, ..)) && args(key, ..);
 pointcut removeCollection(Object key): call(void CollectionRegionAccessStrategy+.remove(Object)) &&  args(key);
 pointcut unlockItemCollection(Object key): call(void CollectionRegionAccessStrategy+.unlockItem(Object, ..))  && args(key, ..); 
 
 boolean around(Object key):
  afterInsertEntity(key) || 
  afterUpdateEntity(key) ||
  insertEntity(key) || 
  putFromLoadEntity(key) || 
  updateEntity(key) ||
  putFromLoadCollection(key){
  Object args = handleCacheKey(key, thisJoinPoint);
  return proceed(args);
 }
 
 void around(Object key):
  evictEntity(key)||
  removeEntity(key) ||
  unlockItemEntity(key){
  Object args = handleCacheKey(key, thisJoinPoint);
  proceed(args);
  return;
 }
 
 void around(Object key):
  evictCollection(key) ||
  removeCollection(key) ||
     unlockItemCollection(key){
  Object args = handleCacheKey(key, thisJoinPoint);
  proceed(args);
  return;
}
 
 Object around(Object key):
  getEntity(key)||
  getCollection(key) ||
  lockItemEntity(key)||
  lockItemCollection(key){
  Object args = handleCacheKey(key, thisJoinPoint);
  return proceed(args);
 }

 
 Object handleCacheKey(Object key, JoinPoint thisJoinPoint)
 {
  if (key instanceof CacheKey)
  {
   CacheKey cacheKey = (CacheKey)key;
   DatabaseDependentCacheKey oKey = new DatabaseDependentCacheKey(cacheKey, CustomerContextHolder.getCustomerType());
   log.debug(oKey.toString());
   return oKey;
  }
  else
  {
   log.debug(key.toString());
   return key;
  }
 }
}

package com.fennek.gem.datasource.lookup;

import java.io.Serializable;

import org.hibernate.cache.CacheKey;

public class DatabaseDependentCacheKey implements Serializable {

 private static final long serialVersionUID = 1L;
 
 private String dbKey; 
 private CacheKey cacheKey; 
 
 public DatabaseDependentCacheKey(CacheKey oKey, String DBKey){
  this.cacheKey = oKey;
  this.dbKey = DBKey;
 }
 
 @Override
 public boolean equals(Object other) {
  if ( !(other instanceof DatabaseDependentCacheKey) ) return false;
  DatabaseDependentCacheKey that = (DatabaseDependentCacheKey) other;
  return dbKey.equals( that.dbKey )
    && cacheKey.equals(that.cacheKey);
 }
 
 @Override
 public int hashCode() {
  return 31 * dbKey.hashCode() + cacheKey.hashCode();
 }

 public String getDatabaseKey() {
  return dbKey;
 }
 
 @Override
 public String toString() {
  return dbKey + '.' + cacheKey.toString();
 }
}

There is still one thing to do. Normally aspects are woven into the classes at compile time. Since we generally only use the compiled code (jar files) we have to set up Load Time Weaving. But this is a blog post by itself.

1 comment: