One application, per client database

In one of my recent projects we came a cross an application model which had 1 codebase but for every client they had (around 40) they deployed one application. Sometimes they had to redeploy several times because they had memory and performance issues. We soon realized that we needed to do something about this way of deploying. The only two things which where different per client where the database connection and the front-end.

Front-end
Well the front-end is simple enough there are enough templating engines around which you can use. We decided on using sitemesh this in combination with JSTL gave us all the power we needed.

Database connection
But what about the database connections. We had 40 of them configured in our tomcat context file. We figured we needed something like the HotSwappableTargetSource. The challenge was how do we decide for which client we need to process something. The application has urls like http://www.ourcomp.com/client1 and http://www.ourcomp.com/client2 etc. We created a filter which extracted the client1 part from the URL and put that in a ContextHolder (which is a ThreadLocal).

public abstract class ContextHolder {

  private static final ThreadLocal holder = new ThreadLocal();

  public static void setContext(String context) {
    LoggerFactory.getLogger(ContextHolder.class).debug("context set '{}'", context);
    holder.set(context);
  }

  public static String getContext() {
    return (String) holder.get();
  }
}

Now we had the context to use in a property we could retrieve anywhere. So next we took the idea of the HotSwappableTargetSource and adapted it for our situation. We created a ContextSwappableTargetSource, we need to create it with the targetClass is provides (in our case a javax.sql.DataSource) and with a map of DataSources to use. The keys in the map correspond to the context (or clientnames) used in the url.

/**
* TargetSource which returns the correct target based on the current context set in the {@link biz.deinum.springframework.core.ContextHolder}.
* If no context is found a {@link TargetLookupFailureException} is thrown or the <code>defaultTarget</code> is returned
* , depending on the setting of the alwaysReturnTarget property (default is false);
*
* @author M. Deinum
* @version 1.0
* @see ContextHolder
* @see TargetSource
*/
public class ContextSwappableTargetSource implements TargetSource, InitializingBean {
  private final Logger logger = LoggerFactory.getLogger(ContextSwappableTargetSource.class);
  private Map targets = Collections.synchronizedMap(new HashMap());
  private Class targetClass;
  private boolean alwaysReturnTarget = false;
  private Object defaultTarget;

  /**
  * Constructor for the {@link ContextSwappableTargetSource} class. It takes a
  * Class as a parameter.
  *
  * @param targetClass The Class which this TargetSource represents.
  */
  public ContextSwappableTargetSource(Class targetClass) {
    super();
    this.targetClass=targetClass;
  }

  /**
  * Locate and return the sessionfactory for the current context.
  *
  * First we lookup the context name from the {@link ContextHolder}
  * this context name is used to lookup the desired target. When none
  * is found we return the default target.
  *
  * If the targetClass is of a invalid type we throw a {@link BeanNotOfRequiredTypeException}
  *
  * @see ContextHolder
  */
  public Object getTarget() throws Exception {
    // Determine the current context name from theclass that holds the
    // context name for the current thread.
    String contextName = ContextHolder.getContext();
    logger.debug("Current context: '{}'", contextName);

    Object target = targets.get(contextName);
    if (target == null && alwaysReturnTarget) {
      logger.debug("Return default target for context '{}'", contextName);
      target = defaultTarget;
    } else if (target == null && !alwaysReturnTarget){
      logger.error("Cannot locate a target of type '{}' for context '{}'", targetClass.getName(), contextName);
      throw new TargetLookupFailureException("Cannot locate a target for context '"+contextName+"'");
    }

    if (!targetClass.isAssignableFrom(target.getClass())) {
      throw new TargetLookupFailureException("The target for '"+contextName+"' is not of the required type." + "Expected '"+targetClass.getName()+"' and got '"+target.getClass().getName()+"'");
    }
    return target;

  }

  public final Class getTargetClass() {
    return targetClass;
  }

  public final boolean isStatic() {
    return false;
  }

  public void releaseTarget(Object arg0) throws Exception {}

  public final void afterPropertiesSet() throws Exception {
    Assert.notNull(targetClass, "TargetClass property must be set!");

    if (alwaysReturnTarget && defaultTarget == null) {
      throw new IllegalStateException("The defaultTarget property is null, while alwaysReturnTarget is set to true. " + "When alwaysReturnTarget is set to true a defaultTarget must be set!");
    }
  }

  public final void setAlwaysReturnTarget(final boolean alwaysReturnTarget) {
    this.alwaysReturnTarget=alwaysReturnTarget;
  }

  public final void setDefaultTarget(final Object defaultTarget) {
    this.defaultTarget=defaultTarget;
  }

  public final void setTargets(final Map targets) {
    this.targets.clear();
    this.targets.putAll(targets);
  }
}

All the classes are in place, now we only needed to wire things up in our application context and we should be good to go. First we configure the datasources.

Next we need to setup the ContextSwappableTargetSource, the key in the map is the value which is going to be set in the ContextHolder. In a WebApplication this value could be set by a ServletFilter on each request. The TargetSource is wrapped in a ProxyFactoryBean so a Proxy will be created for the ContextSwappableTargetSource.

And that is it. Now inject the datasourceTargetSource into a JdbcTemplates datasource property and you are good to go.

At every request the context is set in the ContextHolder. Then at every action on the JdbcTemplate the getTarget method on the ContextSwappableTargetSource is called returning the real and correct datasource instance for that request.

In this example we used it to dynamically replace DataSource instances but this can work with in theory every object. We have also succesfully used it with multiple hibernate SessionFactories.

The source code (as available on posttime) can be found here. Or point yuor favorite SVN client to http://bespring.googlecode.com/svn/trunk/. I submitted this to the Spring framework in JIRA issue SPR-3014

In the meantime the code has moved to GitHub, feel free to fork/use it.

About these ads

3 comments on “One application, per client database

  1. It is nice to stumble upon this! The database side of this is exactly our situation and this is the approach that seems most reasonable to me. The implementation seems like a no brainer in straight JDBC, but we use hibernate, so I look forward to looking at your code.

    Another of our engineers advocates merging all the client databases into a single database and modifying the app accordingly. That sounds like a nightmare to me on several levels, including the risk to customer data.

    I have just started some prototype work on a branch of our application and look forward to letting you know how it goes.

  2. Mike,

    It has been a while but we also used hibernate and the only thing we switched was the datasource. You really don’t want to have 50 hibernate sessionfactories in memory. They are heavy to create. So as long as the schemas are the same use 1 sessionfactory and simply switch the datasource.

    Drawback is that you also would have to do something similair for 2nd level caching (switching caches also) but that shouldn’t be a problem either.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s