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).
[java]
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();
}
}
[/java]
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.
[java]
import biz.deinum.springframework.core.ContextHolder;
/**
* 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 defaultTarget 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);
}
}
[/java]
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.
[xml]
[/xml]
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.
[xml]
[/xml]
And that is it. Now inject the datasourceTargetSource into a JdbcTemplates datasource property and you are good to go.
[xml]
[/xml]
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 also submitted this to the Spring framework in JIRA issue SPR-3014
December 18th, 2007 at 9:39 am
I would possible use much simpler solution - connection customizer (supported by c3p0 that you use anyway AFAIK). And in this customizer issue single SQL-command “set schem DESIRED_SCHEMA” (or, in case of Oracle, “alter session set current_schema=DESIRED_SCHEMA”).
Regards,
Oleksandr
December 18th, 2007 at 9:53 am
@al0 if it was that simple I would have done it but they where also physically different database so on different machines/ports. It wasn’t as easy/simple as switching the schema name.
December 21st, 2007 at 10:36 am
Yes, it was not 100% clear from description that they are different DB instances, not just different schemas.
But 40 session factories seems terrible expensive to create.
BTWmaintability of this solution is as well questionable (what to do if structure for some schemas was updated and for others - not?).
December 21st, 2007 at 11:01 am
In the mean time we already changed some of the implemenation to 1 sessionfactory and ‘just’ 50 datasources, that was the only differences. (We had some increase in clients
).
Regarding the maintainability that isn“t an issue, all the database are in the same version and use the same version of the application. The other option was to deploy an application per client, that would have been a maintainability issue, managing 50 apps, multiple app servers etc. now we have 1 server serving 50 clients. Very managable.
Especially because we only have to create a view, the rest is updated/looked up dynamically. So new view means new client.
May 6th, 2008 at 11:43 am
Hi,
Thanks for the article.
I implemented some functionality that use HotSwappableTargetSource to switch from multiple datasources . The problem is when user1 goes into application he will use dataSource1 for finding some data .When user2 goes into application he will use dataSource2 to find some data , and I have only swappable target datasource , then when the user1 will try to made a new search he will find the datasource swap to datasource2 which is not good .
How can I bound the datasource used from the user session?
Or how can i prevent this from occuring by maybe thread/transaction binding the datasource switching ?
Any suggestions ?
From Cemil