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

2022-09-01 07:49:06

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

}

答案 1

假设

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

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

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

  • 每个架构都已设置了所需的 DDL,因此无需休眠即可创建表或其他内容

  • 每个数据库架构看起来完全相同,除了其名称

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

我的解决方案理念

使用 ThreadLocal 代理的组合来获取模式名称和单例数据源,后者在每个用户请求上的行为都不同。这个解决方案的灵感来自你对Meherzad的评论和自己的经验的暗示。AbstractRoutingDataSource

动态DataSource

我建议促进弹簧并像.我们不是使用类似静态的方法,而是使用番石榴缓存来获得易于使用的缓存。AbstractDataSourceAbstractRoutingDataSourceMap

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();
    }
}

现在你有一个“数据源”,它对每个用户的行为都不同。创建数据源后,它将缓存 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的,您需要将其定义为原型。您可以利用 JSR-330 和 Spring Securitys SecurityContextHolder 的 Springs 支持:Provider<CustomUserDetails>

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

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

这有帮助吗?

编辑2只是为了记录:不要直接引用豆子。由于这是一个原型,Spring将尝试为类创建一个代理 ,这在我们的例子中不是一个好主意。因此,只需使用 s 即可访问此 bean。或者使其成为一个界面。CustomUserDetailsCustomUserDetailsProvider


答案 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获取连接时,它可以允许您运行任意SLL。您只需执行命令即可切换模式(例如 在Oracle中,用于Sybase等)。ALTER SESSION SET CURRENT SCHEMA = 'schemaName'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>

推荐