根据登录用户在运行时更改数据库架构
Posted
技术标签:
【中文标题】根据登录用户在运行时更改数据库架构【英文标题】:Change database schema during runtime based on logged in user 【发布时间】:2017-01-14 09:43:00 【问题描述】:我已经阅读了很多关于动态数据源路由的问题和答案,并使用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");
【问题讨论】:
不想深入研究,可以把所有逻辑都放到返回原型DataSource实例的方法中吗? 对。多模式数据库比多数据库服务器更有意义,这是您在编辑之前提出的问题。 是的,更改了文本,它具有误导性。感谢并为错误的措辞感到抱歉! @大卫华莱士 我绝对想知道这个问题的答案。我有一个非常大的应用程序,需要为特定工作切换模式。但无论何时发生这种特殊情况,它都会挂起。 forum.spring.io/forum/spring-projects/data/… 这涵盖了您需要的大部分内容。用户名可以从之前设置的线程局部变量中获取。 【参考方案1】:假设
因为我还没有在您的问题下方发表评论的声誉,所以我的回答基于以下假设:
用于当前用户的当前模式名称可通过 Spring JSR-330 Provider 访问,例如 private javax.inject.Provider<User> user; String schema = user.get().getSchema();
。理想情况下,这是一个基于 ThreadLocal 的代理。
要构建一个以您需要的方式完全配置的DataSource
,它需要相同的属性。每次。唯一不同的是模式名称。 (也很容易获得其他不同的参数,但这对于这个答案来说太过分了)
每个模式都已经设置了所需的 DDL,因此不需要休眠来创建表或其他东西
每个数据库架构看起来都完全相同,只是名称不同
每次相应用户向您的应用程序发出请求时,您都需要重用 DataSource。但是您不希望每个用户的每个 DataSource 都永久保存在内存中。
我的解决思路
使用 ThreadLocal 代理的组合来获取模式名称和 Singleton-DataSource,它在每个用户请求上的行为都不同。此解决方案的灵感来自您对 AbstractRoutingDataSource
、Meherzad 的 cmets 和自己的经验的提示。
动态DataSource
我建议促进Spring的AbstractDataSource
,并像AbstractRoutingDataSource
一样实现它。我们使用Guava Cache 来获得易于使用的缓存,而不是静态的Map
-like 方法。
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 后,它将被缓存 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();
结论
我们有一个透明的动态数据源,它每次都使用正确的数据源。
开放式问题
当没有用户登录时怎么办?是否不允许访问数据库? 谁制定计划?免责声明
我没有测试过这段代码!
编辑:
要使用 Spring 实现 Provider<CustomUserDetails>
,您需要将其定义为原型。您可以利用 Springs 对 JSR-330 和 Spring Securitys SecurityContextHolder 的支持:
@Bean @Scope("prototype")
public CustomUserDetails customUserDetails()
return return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
您不再需要RequestInterceptor
、UserProvider
或控制器代码来更新用户。
这有帮助吗?
EDIT2
仅作记录:不要直接引用 CustomUserDetails
bean。由于这是一个原型,Spring 将尝试为类CustomUserDetails
创建一个代理,这在我们的例子中不是一个好主意。所以只需使用Provider
s 来访问这个bean。或者让它成为一个界面。
【讨论】:
如果您需要其他数据库的模式感知 jdbc url,例如甲骨文,请告诉我 您好,谢谢您的回答,我认为这是正确的方向。我会再试一会,并会在几分钟后发布我当前的代码。如果你能看看就好了! 已编辑问题! 编辑了我的答案 :-) @alexandrum,不客气 :-) 我很高兴我的回答实际上是在帮助那里的人们【参考方案2】:鉴于您没有指定 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
等)。
例如
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;
在应用上下文配置中
<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>
【讨论】:
我认为我和 Adrians 的解决方案存在问题。我们都忘记了 EntityManagers JDBC 连接的处理。例如OpenJPA(我知道这是一个旧版本,但新版本不会在我的笔记本电脑上加载:-() 默认情况下始终使用相同的连接。所以数据源上的getConnection(...)
只调用一次。
你的意思是OpenJPA自己做另一层连接池?听起来怪怪的。在大多数情况下,你会发现一个 EM 总是使用相同的连接,因为 Datasource 正在做连接池,所以它总是会返回一个池连接。如果是这样的话,我的方法应该可行。无论如何,至少我的方式适用于 Hibernate(我已经使用它来设置 Oracle VPD)以上是关于根据登录用户在运行时更改数据库架构的主要内容,如果未能解决你的问题,请参考以下文章