Guide de l'architecture mutualisée dans Hibernate 5

1. Introduction

L'architecture mutualisée permet à plusieurs clients ou locataires d'utiliser une seule ressource ou, dans le contexte de cet article, une seule instance de base de données. Le but est d'isoler les informations dont chaque locataire a besoin de la base de données partagée .

Dans ce didacticiel, nous présenterons diverses approches de configuration de l'architecture mutualisée dans Hibernate 5.

2. Dépendances de Maven

Nous devrons inclure la dépendance hibernate-core dans le fichier pom.xml :

 org.hibernate hibernate-core 5.2.12.Final 

Pour les tests, nous utiliserons une base de données en mémoire H2, donc ajoutons également cette dépendance au fichier pom.xml :

 com.h2database h2 1.4.196 

3. Compréhension de l'architecture mutualisée dans Hibernate

Comme mentionné dans le guide de l'utilisateur officiel d'Hibernate, il existe trois approches de la mutualisation dans Hibernate:

  • Schéma séparé - un schéma par locataire dans la même instance de base de données physique
  • Base de données distincte - une instance de base de données physique distincte par locataire
  • Données partitionnées (discriminateur) - les données de chaque locataire sont partitionnées par une valeur discriminante

L' approche des données partitionnées (discriminantes) n'est pas encore prise en charge par Hibernate. Suivez ce numéro JIRA pour les progrès futurs.

Comme d'habitude, Hibernate fait abstraction de la complexité autour de la mise en œuvre de chaque approche.

Tout ce dont nous avons besoin est de fournir une implémentation de ces deux interfaces :

  • MultiTenantConnectionProvider - fournit des connexions par locataire

  • CurrentTenantIdentifierResolver - résout l'identifiant de locataire à utiliser

Voyons plus en détail chaque concept avant de parcourir les exemples d'approches de base de données et de schéma.

3.1. MultiTenantConnectionProvider

Fondamentalement, cette interface fournit une connexion à la base de données pour un identifiant de locataire concret.

Voyons ses deux méthodes principales:

interface MultiTenantConnectionProvider extends Service, Wrapped { Connection getAnyConnection() throws SQLException; Connection getConnection(String tenantIdentifier) throws SQLException; // ... }

Si Hibernate ne peut pas résoudre l'identifiant de client à utiliser, il utilisera la méthode getAnyConnection pour obtenir une connexion. Sinon, il utilisera la méthode getConnection .

Hibernate fournit deux implémentations de cette interface en fonction de la façon dont nous définissons les connexions à la base de données:

  • En utilisant l'interface DataSource de Java - nous utiliserions l' implémentation DataSourceBasedMultiTenantConnectionProviderImpl
  • En utilisant l' interface ConnectionProvider d'Hibernate - nous utiliserions l' implémentation AbstractMultiTenantConnectionProvider

3.2. CurrentTenantIdentifierResolver

Il existe de nombreuses façons de résoudre un identifiant de client . Par exemple, notre implémentation pourrait utiliser un identifiant de locataire défini dans un fichier de configuration.

Une autre façon pourrait consister à utiliser l'identificateur de locataire à partir d'un paramètre de chemin.

Voyons cette interface:

public interface CurrentTenantIdentifierResolver { String resolveCurrentTenantIdentifier(); boolean validateExistingCurrentSessions(); }

Hibernate appelle la méthode resolutionCurrentTenantIdentifier pour obtenir l'identificateur de locataire. Si nous voulons qu'Hibernate valide toutes les sessions existantes appartiennent au même identifiant de locataire, la méthode validateExistingCurrentSessions doit retourner true.

4. Approche schématique

Dans cette stratégie, nous utiliserons différents schémas ou utilisateurs dans la même instance de base de données physique. Cette approche doit être utilisée lorsque nous avons besoin des meilleures performances pour notre application et que nous pouvons sacrifier des fonctionnalités de base de données spéciales telles que la sauvegarde par locataire.

En outre, nous allons simuler l' interface CurrentTenantIdentifierResolver pour fournir un identifiant de locataire comme notre choix pendant le test:

public abstract class MultitenancyIntegrationTest { @Mock private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; private SessionFactory sessionFactory; @Before public void setup() throws IOException { MockitoAnnotations.initMocks(this); when(currentTenantIdentifierResolver.validateExistingCurrentSessions()) .thenReturn(false); Properties properties = getHibernateProperties(); properties.put( AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); sessionFactory = buildSessionFactory(properties); initTenant(TenantIdNames.MYDB1); initTenant(TenantIdNames.MYDB2); } protected void initTenant(String tenantId) { when(currentTenantIdentifierResolver .resolveCurrentTenantIdentifier()) .thenReturn(tenantId); createCarTable(); } }

Notre implémentation de l' interface MultiTenantConnectionProvider définira le schéma à utiliser chaque fois qu'une connexion est demandée :

class SchemaMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider { private ConnectionProvider connectionProvider; public SchemaMultiTenantConnectionProvider() throws IOException { this.connectionProvider = initConnectionProvider(); } @Override protected ConnectionProvider getAnyConnectionProvider() { return connectionProvider; } @Override protected ConnectionProvider selectConnectionProvider( String tenantIdentifier) { return connectionProvider; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { Connection connection = super.getConnection(tenantIdentifier); connection.createStatement() .execute(String.format("SET SCHEMA %s;", tenantIdentifier)); return connection; } private ConnectionProvider initConnectionProvider() throws IOException { Properties properties = new Properties(); properties.load(getClass() .getResourceAsStream("/hibernate.properties")); DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl(); connectionProvider.configure(properties); return connectionProvider; } }

Nous allons donc utiliser une base de données H2 en mémoire avec deux schémas - un pour chaque locataire.

Configurons hibernate.properties pour utiliser le mode de gestion mutualisée du schéma et notre implémentation de l' interface MultiTenantConnectionProvider :

hibernate.connection.url=jdbc:h2:mem:mydb1;DB_CLOSE_DELAY=-1;\ INIT=CREATE SCHEMA IF NOT EXISTS MYDB1\\;CREATE SCHEMA IF NOT EXISTS MYDB2\\; hibernate.multiTenancy=SCHEMA hibernate.multi_tenant_connection_provider=\ com.baeldung.hibernate.multitenancy.schema.SchemaMultiTenantConnectionProvider

Pour les besoins de notre test, nous avons configuré la propriété hibernate.connection.url pour créer deux schémas. Cela ne devrait pas être nécessaire pour une application réelle puisque les schémas devraient déjà être en place.

Pour notre test, nous ajouterons une entrée Car dans le locataire myDb1. Nous vérifierons que cette entrée a été stockée dans notre base de données et qu'elle n'est pas dans le locataire myDb2 :

@Test void whenAddingEntries_thenOnlyAddedToConcreteDatabase() { whenCurrentTenantIs(TenantIdNames.MYDB1); whenAddCar("myCar"); thenCarFound("myCar"); whenCurrentTenantIs(TenantIdNames.MYDB2); thenCarNotFound("myCar"); }

Comme nous pouvons le voir dans le test, nous modifions le client lors de l'appel à la méthode whenCurrentTenantIs .

5. Approche de la base de données

L'approche mutualisée de base de données utilise différentes instances de base de données physiques par client . Étant donné que chaque locataire est entièrement isolé, nous devons choisir cette stratégie lorsque nous avons besoin de fonctionnalités de base de données spéciales telles que la sauvegarde par locataire plus que nous n'avons besoin des meilleures performances.

For the Database approach, we'll use the same MultitenancyIntegrationTest class and the CurrentTenantIdentifierResolver interface as above.

For the MultiTenantConnectionProvider interface, we'll use a Map collection to get a ConnectionProvider per tenant identifier:

class MapMultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider { private Map connectionProviderMap = new HashMap(); public MapMultiTenantConnectionProvider() throws IOException { initConnectionProviderForTenant(TenantIdNames.MYDB1); initConnectionProviderForTenant(TenantIdNames.MYDB2); } @Override protected ConnectionProvider getAnyConnectionProvider() { return connectionProviderMap.values() .iterator() .next(); } @Override protected ConnectionProvider selectConnectionProvider( String tenantIdentifier) { return connectionProviderMap.get(tenantIdentifier); } private void initConnectionProviderForTenant(String tenantId) throws IOException { Properties properties = new Properties(); properties.load(getClass().getResourceAsStream( String.format("/hibernate-database-%s.properties", tenantId))); DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl(); connectionProvider.configure(properties); this.connectionProviderMap.put(tenantId, connectionProvider); } }

Each ConnectionProvider is populated via the configuration file hibernate-database-.properties, which has all the connection details:

hibernate.connection.driver_class=org.h2.Driver hibernate.connection.url=jdbc:h2:mem:;DB_CLOSE_DELAY=-1 hibernate.connection.username=sa hibernate.dialect=org.hibernate.dialect.H2Dialect

Finally, let's update the hibernate.properties again to use the database multitenancy mode and our implementation of the MultiTenantConnectionProvider interface:

hibernate.multiTenancy=DATABASE hibernate.multi_tenant_connection_provider=\ com.baeldung.hibernate.multitenancy.database.MapMultiTenantConnectionProvider

Si nous exécutons exactement le même test que dans l'approche de schéma, le test réussit à nouveau.

6. Conclusion

Cet article traite de la prise en charge d'Hibernate 5 pour l'architecture mutualisée à l'aide de la base de données séparée et des approches de schéma distinctes. Nous fournissons des implémentations et des exemples très simplistes pour sonder les différences entre ces deux stratégies.

Les exemples de code complets utilisés dans cet article sont disponibles sur notre projet GitHub.