我已经阅读了许多关于动态数据源路由的问题和答案,并使用AbstractRoutingDataSource
和另一个(见下文)实现了解决方案。这很好,但需要所有数据源的硬编码属性。随着使用该应用程序的用户数量的增加,这不再是一种合适的路由选择方式。每次新用户注册时,还需要在属性中添加一个条目。情况如下
我正在使用spring boot 1.4.0
以及hibernate 5.1
和spring data jpa
我无法找到一种完全动态更改架构的方法。有人知道如何在春天这样做吗?
修改
感谢@Johannes Leimer的回答,我得到了一个有效的实现。
以下是代码:
用户提供商:
@Component
public class UserDetailsProvider {
@Bean
@Scope("prototype")
public CustomUserDetails customUserDetails() {
return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
UserSchemaAwareRoutingDatasource :
public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
@Inject
Provider<CustomUserDetails> customUserDetails;
@Inject
Environment env;
private LoadingCache<String, DataSource> dataSources = createCache();
@Override
public Connection getConnection() throws SQLException {
try {
return determineTargetDataSource().getConnection();
} catch (ExecutionException e){
e.printStackTrace();
return null;
}
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
System.out.println("getConnection" + username);
System.out.println("getConnection2" + password);
try {
return determineTargetDataSource().getConnection(username, password);
} catch (ExecutionException e) {
e.printStackTrace();
return null;
}
}
private DataSource determineTargetDataSource() throws SQLException, ExecutionException {
try {
String schema = customUserDetails.get().getUserDatabase();
return dataSources.get(schema);
} catch (NullPointerException e) {
e.printStackTrace();
return dataSources.get("fooooo");
}
}
答案 0 :(得分:20)
因为我还没有在您的问题下面发表评论的声誉,我的回答是基于以下几点:
可以通过像private javax.inject.Provider<User> user; String schema = user.get().getSchema();
这样的Spring JSR-330提供程序访问当前用户的当前模式名称。理想情况下,这是一个基于ThreadLocal的代理。
要构建以您需要的方式完全配置的DataSource
,需要相同的属性。每次。唯一不同的是模式名称。 (很容易获得其他不同的参数,但这对于这个答案来说太过分了)
每个模式都已使用所需的DDL进行设置,因此不需要hibernate创建表或其他东西
除了名称
每次相应的用户向您的应用程序发出请求时,您都需要重用DataSource。但是你不希望永久地将每个用户的每个数据源都存储在内存中。
使用ThreadLocal代理的组合来获取模式名称和Singleton-DataSource,它在每个用户请求中的行为都不同。这个解决方案的灵感来自于您对AbstractRoutingDataSource
的提示,Meherzad的评论和自己的经验。
DataSource
我建议为AbstractDataSource
Spring提供帮助,并像AbstractRoutingDataSource
一样实现它。我们使用Guava Cache来获取易于使用的缓存,而不是静态Map
方法。
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();
}
}
现在你有一个`DataSource',它对每个用户都有不同的作用。一旦创建了DataSource,它将被缓存10分钟。就是这样。
集成我们新创建的DataSource的地方是Spring上下文已知的DataSource单例,并在所有bean中使用,例如EntityManagerFactory
所以我们需要一个等价物:
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
但它必须比基于普通属性的DataSourceBuilder更具动态性:
@Primary
@Bean(name = "dataSource")
public UserSchemaAwareRoutingDataSource dataSource() {
return new UserSchemaAwareRoutingDataSource();
}
我们有一个透明的动态DataSource,每次都使用正确的DataSource。
我还没有测试过这段代码!
修改强>
要使用Spring实现Provider<CustomUserDetails>
,您需要将其定义为原型。您可以使用JSR-330的Spring支持和Spring Securitys SecurityContextHolder:
@Bean @Scope("prototype")
public CustomUserDetails customUserDetails() {
return return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
您不再需要RequestInterceptor
,UserProvider
或控制器代码来更新用户。
这有帮助吗?
<强> EDIT2 强>
仅供记录:不要直接引用CustomUserDetails
bean。由于这是一个原型,Spring将尝试为类CustomUserDetails
创建一个代理,这在我们的案例中不是一个好主意。所以只需使用Provider
来访问这个bean。或者使它成为一个界面。
答案 1 :(得分:0)
鉴于您没有指定DBMS,这是一个可能有帮助的高级想法。
(虽然我使用Spring Data JDBC-ext作为参考,但使用通用AOP可以轻松采用相同的方法)
请参阅http://docs.spring.io/spring-data/jdbc/docs/current/reference/html/orcl.connection.html,第8.2节
在Spring Data JDBC-ext中,有一个ConnectionPreparer,它允许您在从DataSource获取Connection时运行任意SQL。您只需执行命令即可切换架构(例如,Oracle中的ALTER SESSION SET CURRENT SCHEMA = 'schemaName'
,Sybase的using schemaName
等)。
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;
}
}
在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>