web-dev-qa-db-ja.com

ログインしているユーザーに基づいて実行時にデータベーススキーマを変更する

動的データソースルーティングに関する多くの質問と回答を読み、AbstractRoutingDataSourceと別のソリューションを使用してソリューションを実装しました(以下を参照)。それは問題ありませんが、すべてのデータソースにハードコードされたプロパティが必要です。アプリケーションを使用するユーザーの数が増えるにつれて、これはルーティングの適切な方法ではなくなります。また、新しいユーザーが登録するたびにプロパティにエントリを追加する必要があります。状況は次のとおりです

  • 1台のデータベースサーバー
  • そのサーバー上の多くのスキーマ、すべてのユーザーは独自のスキーマを持っています。
  • 実行時にスキーマ名を変更するだけです
  • ログインしたユーザーはスキーマ名を保持できます

spring boot 1.4.0hibernate 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");
    }

}
26
chris p bacon

仮定

私はあなたの質問の下にコメントを投稿する評判がまだないので、私の答えは次の仮定に基づいています:

  • 現在のユーザーに使用される現在のスキーマ名には、private javax.inject.Provider<User> user; String schema = user.get().getSchema();などのSpring JSR-330プロバイダーからアクセスできます。これは、理想的にはThreadLocalベースのプロキシです。

  • 必要な方法で完全に構成されたDataSourceを構築するには、同じプロパティが必要です。毎回。異なるのはスキーマ名だけです。 (他の異なるパラメーターも簡単に取得できますが、これはこの回答には多すぎます)

  • 各スキーマには必要なDDLがすでに設定されているため、休止状態でテーブルなどを作成する必要はありません。

  • 各データベーススキーマは、名前を除いてまったく同じに見えます。

  • 対応するユーザーがアプリケーションにリクエストを行うたびに、DataSourceを再利用する必要があります。ただし、すべてのユーザーのすべてのDataSourceを永続的にメモリに保持する必要はありません。

私のソリューションのアイデア

ThreadLocalプロキシの組み合わせを使用して、スキーマ名と、ユーザーリクエストごとに異なる動作をするSingleton-DataSourceを取得します。このソリューションは、AbstractRoutingDataSource、Meherzadのコメント、および自身の経験に対するヒントに触発されています。

動的なDataSource

SpringのAbstractDataSourceを容易にし、AbstractRoutingDataSourceのように実装することをお勧めします。静的なMapのようなアプローチの代わりに、使いやすいキャッシュを得るために Guava Cache を使用します。

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コンテキストで認識され、すべてのBeanで使用されるDataSourceシングルトンです。 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があります。

未解決の質問

  • ログインしているユーザーがいない場合の対処方法データベースへのアクセスは許可されていませんか?
  • スキームを設定するのは誰ですか?

免責事項

私はこのコードをテストしていません!

EDIT:SpringでProvider<CustomUserDetails>を実装するには、これをプロトタイプとして定義する必要があります。 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にアクセスします。または、インターフェースにします。

22
Johannes Leimer

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には、DataSourceからConnectionを取得するときに任意のSQLを実行できるConnectionPreparerがあります。コマンドを実行するだけでスキーマを切り替えることができます(例:ALTER SESSION SET CURRENT SCHEMA = 'schemaName' Oracleでは、using schemaName Sybaseなど)。

例えば.

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>
0
Adrian Shum