web-dev-qa-db-fra.com

Gestion du regroupement de connexions dans une application Web multi-locataire avec Spring, Hibernate et C3P0

J'essaie de configurer une application Web multi-locataire, avec (idéalement) la possibilité à la fois pour l'approche séparée par la base de données et séparée par le schéma. Bien que je vais commencer par la séparation de schéma. Nous utilisons actuellement: 

  • Printemps 4.0.0
  • Hibernate 4.2.8
  • Hibernate-c3p0 4.2.8 (qui utilise c3p0-0.9.2.1)
  • et PostgreSQL 9.3 (dont je doute que cela compte vraiment pour l'architecture globale)

La plupart du temps, j'ai suivi ce fil de discussion (à cause de la solution pour @Transactional). Mais je suis un peu perdu dans la mise en œuvre de MultiTenantContextConnectionProvider. Il y a aussi cette question similaire posée ici sur SO, mais il y a certains aspects que je ne peux pas comprendre: 

1) Qu'advient-il du regroupement de connexions? Je veux dire, est-ce géré par Spring ou Hibernate? Je suppose qu'avec ConnectionProviderBuilder - ou comme suggéré - n’importe lequel de ses implémentations, Hibernate est le gars qui le gère.
2) Est-ce une bonne approche que Spring ne gère pas le pooling de connexions? ou est-il même possible que Spring y parvienne?
3) Est-ce le bon chemin pour la mise en œuvre future de la séparation de la base de données et du schéma?

Tous les commentaires ou descriptions sont totalement appréciés.

application-context.xml

<beans>
    ...
    <bean id="dataSource" class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
        <property name="targetDataSource" ref="c3p0DataSource" />
    </bean>

    <bean id="c3p0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="org.postgresql.Driver" />
        ... other C3P0 related config
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="packagesToScan" value="com.webapp.domain.model" />

        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</prop>
                <prop key="hibernate.default_schema">public</prop>

                <prop key="hibernate.multiTenancy">SCHEMA</prop>
                <prop key="hibernate.tenant_identifier_resolver">com.webapp.persistence.utility.CurrentTenantContextIdentifierResolver</prop>
                <prop key="hibernate.multi_tenant_connection_provider">com.webapp.persistence.utility.MultiTenantContextConnectionProvider</prop>
            </props>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="autodetectDataSource" value="false" />
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

   ...
</beans>

CurrentTenantContextIdentifierResolver.Java

public class CurrentTenantContextIdentifierResolver implements CurrentTenantIdentifierResolver {
    @Override
    public String resolveCurrentTenantIdentifier() {
        return CurrentTenantIdentifier;  // e.g.: public, tid130, tid456, ...
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

MultiTenantContextConnectionProvider.Java

public class MultiTenantContextConnectionProvider extends AbstractMultiTenantConnectionProvider {
    // Do I need this and its configuratrion?
    //private C3P0ConnectionProvider connectionProvider = null;

    @Override
    public ConnectionProvider getAnyConnectionProvider() {
        // the main question is here.
    }

    @Override
    public ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
        // and of course here.
    }
}



Modifier 

Concernant la réponse de @ ben75: 

Ceci est une nouvelle implémentation de MultiTenantContextConnectionProvider. Il ne s'étend plus AbstractMultiTenantConnectionProvider. Il implémente plutôt MultiTenantConnectionProvider, pour pouvoir renvoyer [Connection][4] au lieu de [ConnectionProvider][5]

public class MultiTenantContextConnectionProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService {
    private DataSource lazyDatasource;;

    @Override
    public void injectServices(ServiceRegistryImplementor serviceRegistry) {
        Map lSettings = serviceRegistry.getService(ConfigurationService.class).getSettings();

        lazyDatasource = (DataSource) lSettings.get( Environment.DATASOURCE );
    }

    @Override
    public Connection getAnyConnection() throws SQLException {
        return lazyDatasource.getConnection();
    }

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        final Connection connection = getAnyConnection();

        try {
            connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'");
        }
        catch (SQLException e) {
            throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier + "]", e);
        }

        return connection;
    }
}
26
Khosrow

Vous pouvez choisir entre 3 stratégies différentes qui auront un impact sur l'interrogation de connexion. Dans tous les cas, vous devez fournir une implémentation de MultiTenantConnectionProvider . La stratégie que vous choisirez aura bien sûr un impact sur votre mise en œuvre.

Remarque générale à propos de MultiTenantConnectionProvider.getAnyConnection()

getAnyConnection() est requis par hibernate pour collecter les métadonnées et configurer la SessionFactory. Généralement, dans une architecture à plusieurs locataires, vous avez une base de données spéciale/principale (ou un schéma) non utilisée par aucun locataire. C'est une sorte de base de données modèle (ou schéma). C'est ok si cette méthode retourne une connexion à cette base de données (ou schéma).

Stratégie 1: chaque locataire a sa propre base de données. (et donc c'est son propre pool de connexion)

Dans ce cas, chaque locataire a son propre pool de connexions géré par C3PO et vous pouvez fournir une implémentation de MultiTenantConnectionProvider basée sur AbstractMultiTenantConnectionProvider

Chaque locataire a son propre C3P0ConnectionProvider , il ne vous reste donc plus qu'à retourner dans selectConnectionProvider(tenantIdentifier) le correct. Vous pouvez conserver une carte pour les mettre en cache et vous pouvez initialiser en différé un fournisseur C3POConnection avec quelque chose comme:

private ConnectionProvider lazyInit(String tenantIdentifier){
    C3P0ConnectionProvider connectionProvider = new C3P0ConnectionProvider();
    connectionProvider.configure(getC3POProperties(tenantIdentifier));
    return connectionProvider;
}

private Map getC3POProperties(String tenantIdentifier){
    // here you have to get the default hibernate and c3po config properties 
    // from a file or from Spring application context (there are good chances
    // that those default  properties point to the special/master database) 
    // and alter them so that the datasource point to the tenant database
    // i.e. : change the property hibernate.connection.url 
    // (and any other tenant specific property in your architecture like :
    //     hibernate.connection.username=tenantIdentifier
    //     hibernate.connection.password=...
    //     ...) 
}

Stratégie 2: chaque client hébergé possède son propre schéma et son propre pool de connexions dans une base de données unique

Ce cas est très similaire à la première stratégie concernant la mise en œuvre de ConnectionProvider puisque vous pouvez également utiliser AbstractMultiTenantConnectionProvider comme classe de base pour implémenter votre MultiTenantConnectionProvider

L'implémentation est très similaire à l'implémentation suggérée pour la stratégie 1, sauf que vous devez modifier le schéma à la place de la base de données dans la configuration c3po.

Stratégie 3: chaque client hébergé possède son propre schéma dans une base de données unique mais utilise un pool de connexions partagées

Ce cas est légèrement différent, car chaque locataire utilisera le même fournisseur de connexion (le pool de connexions sera donc partagé). Dans le cas où: le fournisseur de connexion doit définir le schéma à utiliser avant toute utilisation de la connexion. c'est-à-dire que vous devez implémenter MultiTenantConnectionProvider.getConnection(String tenantIdentifier) (c'est-à-dire que l'implémentation par défaut fournie par AbstractMultiTenantConnectionProvider ne fonctionnera pas).

Avec postgresql vous pouvez le faire avec:

 SET search_path to <schema_name_for_tenant>;

ou en utilisant l'alias

 SET schema <schema_name_for_tenant>;

Alors, voici à quoi ressemblera votre getConnection(tenant_identifier);:

@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
    final Connection connection = getAnyConnection();
    try {
        connection.createStatement().execute( "SET search_path TO " + tenanantIdentifier );
    }
    catch ( SQLException e ) {
        throw new HibernateException(
                "Could not alter JDBC connection to specified schema [" +
                        tenantIdentifier + "]",
                e
        );
    }
    return connection;
}

La référence utile est ici (doc officiel)

Autre lien utile C3POConnectionProvider.Java


Vous pouvez combiner la stratégie 1 et la stratégie 2 dans votre mise en œuvre. Vous avez juste besoin d'un moyen de trouver les propriétés de connexion/URL de connexion correctes pour le locataire actuel.


MODIFIER

Je pense que le choix entre la stratégie 2 ou 3 dépend du trafic et du nombre de locataires de votre application. Avec des pools de connexions distincts: le nombre de connexions disponibles pour un locataire sera beaucoup plus faible et donc: si, pour une raison quelconque, un locataire a soudainement besoin de plusieurs connexions, les performances de ce locataire en particulier diminueront considérablement impacté).

En revanche, avec la stratégie 3, si pour une raison quelconque, un locataire a soudainement besoin de plusieurs connexions: la performance observée par chaque locataire diminuera.

En général, je pense que la stratégie 2 est plus flexible et plus sûre: chaque locataire ne peut pas consommer plus d'un montant de connexion donné (et ce montant peut être configuré par locataire si vous en avez besoin).

33
ben75

IMHO, la gestion du pool de connexions sera gérée par défaut par le serveur SQL lui-même. Cependant, certains langages de programmation tels que C # offrent des moyens de contrôler les pools. Reportez-vous ici

Le choix du schéma (1) ou de la base de données distincte (2) pour un client dépend du volume de données que vous pouvez anticiper pour le client. Cependant, les considérations suivantes peuvent valoir la peine d’être examinées.

  1. créer un modèle de schéma partagé pour les clients à l'essai et les clients à faible volume Il peut être identifié par le nombre de fonctionnalités que vous fournissez à un locataire lors du processus d'intégration de un client

  2. lorsque vous créez ou intégrez un client au niveau entreprise qui peut avoir de grandes données transactionnelles, il est idéal de choisir une base de données séparée

  3. Le modèle de schéma peut avoir une implémentation différente pour SQL Server Et une autre pour le serveur MySQL, à prendre en compte.

  4. tenez également compte du fait qu'un client [locataire] peut être disposé à prendre de l'expansion après une période considérable d'utilisation du système. Si aucune option de montée en charge appropriée n'est prise en charge dans votre application, vous devrez être dérangé.

Partagez vos commentaires sur les points ci-dessus pour approfondir cette discussion

0
Saravanan