I\'ve read many questions and answers about dynamic datasource routing and have implemented a solution using AbstractRoutingDataSource
and another(see below). T
Given that you do not specify the DBMS, here is a high-level idea that may help.
(Although I am using Spring Data JDBC-ext as reference, same approach can be easily adopted by using general AOP)
Please refer to http://docs.spring.io/spring-data/jdbc/docs/current/reference/html/orcl.connection.html , Section 8.2
In Spring Data JDBC-ext, there is ConnectionPreparer that can allow you to run arbitrary SQLs when you acquire a Connection from DataSource. You can simply execute the commands to switch schema (e.g. ALTER SESSION SET CURRENT SCHEMA = 'schemaName'
in Oracle, using schemaName
for Sybase etc).
e.g.
package foo;
import org.springframework.data.jdbc.support.ConnectionPreparer;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
public class SwitchSchemaConnectionPreparer implements ConnectionPreparer {
public Connection prepare(Connection conn) throws SQLException {
String schemaName = whateverWayToGetTheScehmaToSwitch();
CallableStatement cs = conn.prepareCall("ALTER SESSION SET CURRENT SCHEMA " + scehmaName);
cs.execute();
cs.close();
return conn;
}
}
In App Context config
<aop:config>
<aop:advisor
pointcut="execution(java.sql.Connection javax.sql.DataSource.getConnection(..))"
advice-ref="switchSchemaInterceptor"/>
</aop:config>
<bean id="switchSchemaInterceptor"
class="org.springframework.data.jdbc.aop.ConnectionInterceptor">
<property name="connectionPreparer">
<bean class="foo.SwitchSchemaConnectionPreparer"/>
</property>
</bean>
Because I don't have the reputation yet to post a comment below your question, my answer is based on the following assumptions:
The current schema name to be used for the current user is accessible through a Spring JSR-330 Provider like private javax.inject.Provider<User> user; String schema = user.get().getSchema();
. This is ideally a ThreadLocal-based proxy.
To build a DataSource
which is fully configured in a way you need it requires the same properties. Every time. The only thing which is different is the schema name. (It would easily possible to obtain other different parameters as well, but this would be too much for this answer)
Each schema is already set up with the needed DDL, so there is no need for hibernate to create tables or something else
Each database schema looks completely the same except for its name
You need to reuse a DataSource every time the corresponding user makes a request to your application. But you don't want to have every DataSource of every user permanently in the memory.
Use a combination of ThreadLocal proxys to get the schema name and a Singleton-DataSource which behaves different on every user request. This solution is inspired by your hint to AbstractRoutingDataSource
, Meherzad's comments and own experience.
DataSource
I suggest to facilitate the AbstractDataSource
of Spring and implement it like the AbstractRoutingDataSource
. Instead of a static Map
-like approach we use a Guava Cache to get an easy to use cache.
public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
private @Inject javax.inject.Provider<User> user;
private @Inject Environment env;
private LoadingCache<String, DataSource> dataSources = createCache();
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
private DataSource determineTargetDataSource() {
String schema = user.get().getSchema();
return dataSources.get(schema);
}
private LoadingCache<String, DataSource> createCache() {
return CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(
new CacheLoader<String, DataSource>() {
public DataSource load(String key) throws AnyException {
return buildDataSourceForSchema(key);
}
});
}
private DataSource buildDataSourceForSchema(String schema) {
// e.g. of property: "jdbc:postgresql://localhost:5432/mydatabase?currentSchema="
String url = env.getRequiredProperty("spring.datasource.url") + schema;
return DataSourceBuilder.create()
.driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
[...]
.url(url)
.build();
}
}
Now you have a `DataSource´ which acts different for every user. Once a DataSource is created it's gonna be cached for 10 minutes. That's it.
The place to integrate our newly created DataSource is the DataSource singleton known to the spring context and used in all beans e.g. the EntityManagerFactory
So we need an equivalent to this:
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
but it has to be more dynamic, than a plain property based DataSourceBuilder:
@Primary
@Bean(name = "dataSource")
public UserSchemaAwareRoutingDataSource dataSource() {
return new UserSchemaAwareRoutingDataSource();
}
We have a transparent dynamic DataSource which uses the correct DataSource everytime.
I haven't tested this code!
EDIT:
To implement a Provider<CustomUserDetails>
with Spring you need to define this as prototype. You can utilize Springs support of JSR-330 and Spring Securitys SecurityContextHolder:
@Bean @Scope("prototype")
public CustomUserDetails customUserDetails() {
return return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
You don't need a RequestInterceptor
, the UserProvider
or the controller code to update the user anymore.
Does this help?
EDIT2
Just for the record: do NOT reference the CustomUserDetails
bean directly. Since this is a prototype, Spring will try to create a proxy for the class CustomUserDetails
, which is not a good idea in our case. So just use Provider
s to access this bean. Or make it an interface.