在运行时根据登录用户更改数据库模式

我已经阅读了许多关于动态数据源路由的问题和答案,并使用AbstractRoutingDataSource和另一个实现了解决方案(见下文)。 这很好,但需要所有数据源的硬编码属性。 随着使用该应用程序的用户数量的增加,这不再是一种合适的路由选择方式。 每次新用户注册时,还需要向属性添加条目。 情况如下

  • 1数据库服务器
  • 该服务器上有许多模式,每个用户都有自己的模式。
  • 我只需要在运行时更改模式名称
  • 架构名称可由登录用户保留

我正在使用spring boot 1.4.0以及hibernate 5.1spring data jpa

我无法找到一种完全动态更改架构的方法。 有人知道如何在spring这样做吗?

编辑:

感谢@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; @Inject Environment env; private LoadingCache 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"); } } 

假设

因为我还没有在你的问题下面发表评论的声誉,我的答案基于以下假设:

  • 可以通过Spring JSR-330 Provider(如private javax.inject.Provider user; String schema = user.get().getSchema(); )访问当前用户的当前模式名称private javax.inject.Provider user; String schema = user.get().getSchema(); private javax.inject.Provider user; String schema = user.get().getSchema(); 。 理想情况下,这是一个基于ThreadLocal的代理。

  • 要构建一个以您需要的方式完全配置的DataSource ,需要相同的属性。 每次。 唯一不同的是模式名称。 (很容易获得其他不同的参数,但这对于这个答案来说太过分了)

  • 每个模式都已使用所需的DDL进行设置,因此不需要hibernate来创建表或其他东西

  • 除了名称之外,每个数据库模式看起来完全相同

  • 每次相应的用户向您的应用程序发出请求时,您都需要重用DataSource。 但是您不希望将每个用户的每个DataSource永久保存在内存中。

我的解决方案

使用ThreadLocal代理的组合来获取模式名称和Singleton-DataSource,它在每个用户请求上的行为都不同。 此解决方案的灵感来自您对AbstractRoutingDataSource的提示,Meherzad的评论和自己的经验。

动态DataSource

我建议使用Spring的AbstractDataSource ,并像AbstractRoutingDataSource一样实现它。 我们使用Guava Cache来获得易于使用的缓存,而不是静态类Map方法。

 public class UserSchemaAwareRoutingDataSource extends AbstractDataSource { private @Inject javax.inject.Provider user; private @Inject Environment env; private LoadingCache 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 createCache() { return CacheBuilder.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .build( new CacheLoader() { public DataSource load(String key) throws AnyException { return buildDataSourceForSchema(key); } }); } private DataSource buildDataSourceForSchema(String schema) { // eg 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

集成我们新创建的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 ,您需要将其定义为原型。 您可以使用JSR-330和Spring Securitys SecurityContextHolder的Springs支持:

 @Bean @Scope("prototype") public CustomUserDetails customUserDetails() { return return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); } 

您不再需要RequestInterceptorUserProvider或控制器代码来更新用户。

这有帮助吗?

EDIT2仅供记录:不要直接引用CustomUserDetails bean。 由于这是一个原型,Spring将尝试为CustomUserDetails类创建一个代理,这在我们的案例中不是一个好主意。 所以只需使用Provider来访问这个bean。 或者使它成为一个界面。

鉴于您没有指定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。 您可以简单地执行命令来切换模式(例如, using schemaName Oracle的using schemaName等,在Oracle中using schemaName ALTER SESSION SET CURRENT SCHEMA = '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; } } 

在App Context配置中