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

时间:2016-09-06 20:30:50

标签: java spring hibernate spring-mvc jpa

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

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

我正在使用spring boot 1.4.0以及hibernate 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");
    }

}

2 个答案:

答案 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

集成我们新创建的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();
}

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

这有帮助吗?

<强> 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>