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");
}
}
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 Map
comme 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.
é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>