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 en utilisant et une Autre (voir ci-dessous). C'est bien, mais nécessite des propriétés codées pour toutes les sources de données. Comme le nombre d'utilisateurs utilisant l'application augmente, ce n'est plus un moyen approprié de routage. Il faudrait aussi ajouter une entrée aux propriétés chaque fois qu'un nouvel utilisateur s'enregistre. La situation est la suivante

  • 1 base de données le serveur
  • beaucoup de schémas sur ce serveur, chaque utilisateur a son propre schéma.
  • je n'ai besoin de changer le nom du schéma en cours d'exécution
  • le nom du schéma peut être récupéré par l'utilisateur connecté

j'utilise spring boot 1.4.0 avec hibernate 5.1 et spring data jpa

Je n'arrive pas à trouver un moyen de changer le schéma complètement dynamiquement. Quelqu'un sait-il comment le faire au printemps?

EDIT:

merci à @Johannes La réponse de Leimer, j'ai une implantation qui marche.

Voici le code:

Utilisateur Fournisseur:

@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");
    }

}
22
demandé sur bambam 2016-09-06 23:30:49

2 réponses

suppositions

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 suppositions suivantes:

  • le nom de schéma courant à utiliser pour l'utilisateur courant est accessible via un fournisseur de printemps 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 avez besoin il nécessite la même propriété. Chaque fois. La seule chose qui est différente est le nom du schéma. (Il serait facilement possible d'obtenir d'autres paramètres, mais ce serait trop pour cette réponse)

  • chaque schéma est déjà configuré avec le DDL nécessaire, donc il n'y a pas besoin d'hibernation pour créer des tables ou autre chose

  • chaque schéma de base de données ressemble complètement le même sauf pour son nom

  • Vous avez besoin pour réutiliser une source de données à chaque fois que l'utilisateur correspondant fait une demande à votre application. Mais vous ne voulez pas avoir toutes les sources de données de chaque utilisateur de façon permanente dans la mémoire.

Ma solution idée

utilisez une combinaison de ThreadLocal proxys pour obtenir le nom du schéma et une source de données unique qui se comporte différemment sur chaque demande d'utilisateur. Cette solution est inspirée par votre allusion à AbstractRoutingDataSource, les commentaires de Meherzad et ses propres expérience.

dynamique DataSource

je suggère de faciliter l' AbstractDataSource de printemps et l'implémenter comme le AbstractRoutingDataSource. Au lieu d'un statique Mapcomme approche, nous utilisons un Goyave 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();
    }
}

Maintenant vous avez une ' source de données qui agit différemment pour chaque utilisateur. Une fois qu'une source de données est créée, elle sera mise en cache pendant 10 minutes. C'est tout.

faire prendre conscience à l'application de notre dynamique Source de données

l'endroit pour intégrer notre nouvelle source de données est la source de données singleton connu au contexte de printemps et utilisé dans tous les haricots, par exemple L'EntityManagerFactory

il nous faut donc 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 simple datasourcebuilder basé sur la propriété:

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

Conclusion

nous avons une source de données dynamique transparente qui utilise la bonne source de données à chaque fois.

questions ouvertes

  • Que faire, lorsque aucun utilisateur n'est connecté? Il n'y a pas de base de données accès autorisé?
  • qui établit les stratagèmes?

Avertissement

je n'ai pas testé ce code!

EDIT: Pour mettre en œuvre un Provider<CustomUserDetails> avec Spring vous devez définir cela comme un prototype. Vous pouvez utiliser le support de ressorts de JSR-330 et Spring Securitys SecurityContextHolder:

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

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

est-ce que cela aide?

EDIT2 Juste pour information: ne pas mentionner le CustomUserDetails haricot directement. Puisqu'il s'agit d'un prototype, Spring va essayer de créer un proxy pour la classe CustomUserDetails, qui n'est pas une bonne idée dans notre cas. Juste utiliser Provider s pour accéder à ce haricot. Ou faire une interface.

15
répondu Johannes Leimer 2016-09-15 12:22:21

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

(Même si je suis en utilisant le Printemps de Données JDBC-ext comme référence, la même approche peut être facilement adopté par l'utilisation générale de l'AOP)

s'il vous Plaît se référer à http://docs.spring.io/spring-data/jdbc/docs/current/reference/html/orcl.connection.html la Section 8.2

dans les données de printemps JDBC-ext, il y a ConnectionPreparer qui peut vous permettre d'exécuter des SQL arbitraires lorsque vous acquérir 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' Oracle, using schemaName pour Sybase etc).

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

dans le contexte de L'application 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>
0
répondu Adrian Shum 2016-09-15 07:23:13