Tech tutorials Spring DataSource Configuration Tutorial
By Insight Editor / 11 Nov 2016 , Updated on 12 Dec 2019 / Topics: Application development
By Insight Editor / 11 Nov 2016 , Updated on 12 Dec 2019 / Topics: Application development
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 in to 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 shouldn’t 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. I planned 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.
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 multitenant problem. That’s to say you have a single application but multiple businesses/users/tenants, each requiring their own data set while using the same business logic. I wanted to go one step further, however. I wanted to be able to create DataSources at runtime, initialize the schema and data, and return those.
In this contrite example, we’re using hypersonic 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, we implemented the protected Object determineCurrentLookupKey() method.
We borrowed from Spring’s pet shop 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.
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. To reset the data, all we needed to do was drop the DataSource from the map or re-initialize it.