根据登录用户在运行时更改数据库架构

Posted

技术标签:

【中文标题】根据登录用户在运行时更改数据库架构【英文标题】:Change database schema during runtime based on logged in user 【发布时间】:2017-01-14 09:43:00 【问题描述】:

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

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

我将spring boot 1.4.0hibernate 5.1spring 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&lt;User&gt; 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&lt;CustomUserDetails&gt;,您需要将其定义为原型。您可以利用 Springs 对 JSR-330 和 Spring Securitys SecurityContextHolder 的支持:

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

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

这有帮助吗?

EDIT2 仅作记录:不要直接引用 CustomUserDetails bean。由于这是一个原型,Spring 将尝试为类CustomUserDetails 创建一个代理,这在我们的例子中不是一个好主意。所以只需使用Providers 来访问这个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)

以上是关于根据登录用户在运行时更改数据库架构的主要内容,如果未能解决你的问题,请参考以下文章

亿级用户架构系统用户登录为啥很快

流数据架构

DRF+Vue实现前后端分离架构登录

监控雪花数据库架构更改的最佳方法?

Web服务------LNMP架构的安装(Nginx,Mysql,PHP,Discus论坛 安装详解)

根据用户登录动态更改数据库