Smarter ideas worth writing about.

Java Data Delegator

Recently, a unique ask came across my inbox: Take an existing application and move it to a training environment. Here’s where the unique part comes in, each user logging into the application should have its own set of data. That data should be initialized to a clean and known state, and the user’s actions should not interfere with anyone else using the application. We started scratching our heads, and a few ideas started to form. My initial thought was to create our own dataSource. My plan was to grab the SecurityContext within the dataSource, and return the actual dataSource that corresponded with that user. Then a colleague of mine pointed me to this fantastic abstract class, AbstractRoutingDataSource. Don’t you just love Spring? Every time there is a problem to solve, they seem to have already done the hard work, and you just need to implement it. This class provides a method protected Object determineCurrentLookupKey(), which returns a key to a Map that contains the appropriate dataSource object. This way, you can configure the dataSources in your XML configuration, and look them up by the value returned.


    <bean id="dataSourceStore1" class="org.apache.tomcat.jdbc.pool.DataSource"
        p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:petclinic-store1"
        p:username="store1Username" p:password="somePassword" />
    <bean id="dataSourceStore2" class="org.apache.tomcat.jdbc.pool.DataSource"
        p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:petclinic-store2"
        p:username="store2Username" p:password="somePassword" />
    <bean id="dataSourceStore3" class="org.apache.tomcat.jdbc.pool.DataSource"
        p:driverClassName="org.hsqldb.jdbcDriver" p:url="jdbc:hsqldb:mem:petclinic-store3"
        p:username="store3Username" p:password="somePassword" />

    <bean id="dataSource" class="com.cardinal.datasource.RoutingDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="store1" value-ref="dataSourceStore1" />
                <entry key="store2" value-ref="dataSourceStore2" />
                <entry key="store3" value-ref="dataSourceStore3" />
            </map>
        </property>
    </bean>

This approach is a great solution to the multi-tenant problem. That is to say you have a single application, but multiple business/users/tenants. Each requiring their own data set, but utilizing the same business logic. I wanted to go one step further, however. I wanted to be able to create dataSources at run time, initialize the schema and data, and return those. In this contrite example, we’re utilizing Hypersonic’s in memory databases, generating a huge memory leak, but it shows the concept. After a couple of overridden methods we had a solution. First, we had to override the protected DataSource determineTargetDataSource() method, because the AbstractRoutingDataSource keeps its targetDataSources private and makes a copy of it in afterPropertiesSet. Next we needed to override afterPropertiesSet to set our instance of the targetDataSources map to that of the super class. Finally, implement the protected Object determineCurrentLookupKey() method. We borrowed from Spring’s petshop example, with some simplification of the dataSources, but in the end we had a new dataSource, with a new Hypersonic database instance being created for every request.

    <bean id="dataSource" class="com.cardinal.datasource.RoutingDataSource"></bean>
package com.cardinal.datasource;

import java.util.HashMap;
import java.util.Map;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Value("${jdbc.driverClassName}")
    private String driverClassName;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;

    protected Map<Object, Object> targetDataSources;

    @Override
    public void afterPropertiesSet() {
        targetDataSources = new HashMap<Object, Object>();
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String key = Math.random()+"";
        if(!targetDataSources.containsKey(key)){
            DataSource datasource = new DataSource();
            datasource.setDriverClassName(driverClassName);
            datasource.setUrl(url+key);
            datasource.setUsername(username);
            datasource.setPassword(password);

            //initialize DB
            //TODO this should be cleaned up, using property injectors for the files
            DataSourceInitializer dsi = new DataSourceInitializer();
            dsi.setDataSource(datasource);
            ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
            populator.addScript(new ClassPathResource("db/hsqldb/initDB.sql"));
            populator.addScript(new ClassPathResource("db/hsqldb/populateDB.sql"));
            DatabasePopulatorUtils.execute(populator, datasource);

            logger.debug("CREATING DATASOURCE");
            logger.debug(driverClassName);
            logger.debug(url+key);
            logger.debug(username);
            logger.debug(password);
            logger.debug(datasource.toString());

            targetDataSources.put(key, datasource);
        }
        return key;
    }

    protected DataSource determineTargetDataSource() {
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = (DataSource) this.targetDataSources.get(lookupKey);

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }
}

Hidden in there is another key component of the ask; database initialization to a pristine state. And to reset the data, all we’ll need to do is to drop the dataSource from the map or re-initialize it.


Share:

About The Author

Consultant
Rusty is an Enterprise Java consultant and Scrum Master who is passionate about enterprise mobile development.