web-dev-qa-db-fra.com

Modifier le schéma de la base de données pendant l'exécution en fonction de l'utilisateur connecté

J'ai lu de nombreuses questions et réponses sur le routage dynamique des sources de données et j'ai implémenté une solution utilisant AbstractRoutingDataSource et une autre (voir ci-dessous). C'est bien, mais nécessite des propriétés codées en dur pour toutes les sources de données. À mesure que le nombre d'utilisateurs utilisant l'application augmente, ce n'est plus un moyen de routage approprié. Il faudrait également ajouter une entrée aux propriétés chaque fois qu'un nouvel utilisateur s'enregistre. La situation est la suivante

  • 1 serveur de base de données
  • de nombreux schémas sur ce serveur, chaque utilisateur a son propre schéma.
  • J'ai seulement besoin de changer le nom du schéma pendant l'exécution
  • le nom du schéma peut être conservé par l'utilisateur connecté

J'utilise spring boot 1.4.0 ensemble avec hibernate 5.1 et spring data jpa

Je ne trouve pas de moyen de changer le schéma de manière complètement dynamique. Est-ce que quelqu'un sait comment le faire au printemps?

MODIFIER:

Grâce à la réponse de @Johannes Leimer, j'ai obtenu une implémentation fonctionnelle.

Voici le code:

Fournisseur d'utilisateur :

@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

Hypothèses

Parce que je n'ai pas encore la réputation de poster un commentaire sous votre question, ma réponse est basée sur les hypothèses suivantes:

  • Le nom de schéma actuel à utiliser pour l'utilisateur actuel est accessible via un fournisseur Spring JSR-330 comme private javax.inject.Provider<User> user; String schema = user.get().getSchema();. Il s'agit idéalement d'un proxy basé sur ThreadLocal.

  • Pour construire un DataSource qui est entièrement configuré d'une manière dont vous en avez besoin, il nécessite les mêmes propriétés. À chaque fois. La seule chose qui est différente est le nom du schéma. (Il serait également possible d'obtenir facilement d'autres paramètres différents, mais ce serait trop pour cette réponse)

  • Chaque schéma est déjà configuré avec le DDL nécessaire, il n'est donc pas nécessaire de mettre en veille prolongée pour créer des tables ou autre chose

  • Chaque schéma de base de données est complètement identique à l'exception de son nom

  • Vous devez réutiliser un DataSource chaque fois que l'utilisateur correspondant fait une demande à votre application. Mais vous ne voulez pas que chaque DataSource de chaque utilisateur soit en mémoire en permanence.

Mon idée de solution

Utilisez une combinaison de proxys ThreadLocal pour obtenir le nom du schéma et un Singleton-DataSource qui se comporte différemment à chaque demande de l'utilisateur. Cette solution est inspirée de votre suggestion de AbstractRoutingDataSource, des commentaires de Meherzad et de votre propre expérience.

Un DataSource dynamique

Je suggère de faciliter le AbstractDataSource de Spring et de l'implémenter comme le AbstractRoutingDataSource. Au lieu d'une approche statique Map, nous utilisons un Guava Cache pour obtenir un cache facile à utiliser.

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

Vous disposez maintenant d'une "DataSource" qui agit différemment pour chaque utilisateur. Une fois qu'une DataSource est créée, elle sera mise en cache pendant 10 minutes. C'est ça.

Faire connaître l'application à notre DataSource dynamique

L'endroit pour intégrer notre DataSource nouvellement créé est le singleton DataSource connu dans le contexte du printemps et utilisé dans tous les beans, par ex. EntityManagerFactory

Nous avons donc besoin d'un équivalent à ceci:

@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
    return DataSourceBuilder.create().build();
}

mais il doit être plus dynamique qu'un DataSourceBuilder basé sur une simple propriété:

@Primary
@Bean(name = "dataSource")
public UserSchemaAwareRoutingDataSource dataSource() {
    return new UserSchemaAwareRoutingDataSource();
}

Conclusion

Nous avons un DataSource dynamique transparent qui utilise le bon DataSource à chaque fois.

Questions ouvertes

  • Que faire quand aucun utilisateur n'est connecté? Aucun accès à la base de données n'est-il autorisé?
  • Qui met en place les programmes?

Avertissement

Je n'ai pas testé ce code!

EDIT: Pour implémenter un Provider<CustomUserDetails> avec Spring, vous devez le définir comme prototype. Vous pouvez utiliser le support Springs de JSR-330 et Spring Securitys SecurityContextHolder:

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

Vous n'avez plus besoin d'un RequestInterceptor, du UserProvider ou du code du contrôleur pour mettre à jour l'utilisateur.

Est-ce que cela aide?

EDIT2 Pour mémoire: ne référencez PAS directement le bean CustomUserDetails. Comme il s'agit d'un prototype, Spring tentera de créer un proxy pour la classe CustomUserDetails, ce qui n'est pas une bonne idée dans notre cas. Il suffit donc d'utiliser Providers pour accéder à ce bean. Ou faites-en une interface.

22
Johannes Leimer

Étant donné que vous ne spécifiez pas le SGBD, voici une idée de haut niveau qui peut vous aider.

(Bien que j'utilise Spring Data JDBC-ext comme référence, la même approche peut être facilement adoptée en utilisant l'AOP général)

Veuillez vous référer à http://docs.spring.io/spring-data/jdbc/docs/current/reference/html/orcl.connection.html , section 8.2

Dans Spring Data JDBC-ext, il existe ConnectionPreparer qui peut vous permettre d'exécuter des SQL arbitraires lorsque vous acquérez une connexion à partir de DataSource. Vous pouvez simplement exécuter les commandes pour changer de schéma (par exemple ALTER SESSION SET CURRENT SCHEMA = 'schemaName' dans Oracle, using schemaName pour Sybase, etc.).

par exemple.

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

Configuration dans le contexte de l'application

<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