Guide de la mutualité dans Hibernate 5

Guide de la multi-location dans Hibernate 5

1. introduction

Multitenancy 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 estto isolate the information each tenant needs from the shared database.

Dans ce tutoriel, nous allons présenter différentes approchesto configuring multitenancy in Hibernate 5.

2. Dépendances Maven

Nous devrons inclurethe hibernate-core dependency dans le fichierpom.xml:


   org.hibernate
   hibernate-core
   5.2.12.Final

Pour les tests, nous allons utiliser une base de données en mémoire H2, donc ajoutons égalementthis dependency au fichierpom.xml:


   com.h2database
   h2
   1.4.196

3. Comprendre l'architecture mutualisée dans Hibernate

Comme mentionné dans lesHibernate User Guideofficiels, il existe trois approches de la mutualisation dans Hibernate:

  • Separate Schema – un schéma par locataire dans la même instance de base de données physique

  • Separate Database – une instance de base de données physique distincte par locataire

  • Partitioned (Discriminator) Data – les données de chaque locataire sont partitionnées par une valeur discriminante

LesPartitioned (Discriminator) Data approach isn’t yet supported by Hibernate. font le suivi dethis JIRA issue pour les progrès futurs.

Comme d'habitude, Hibernate résume la complexité de la mise en œuvre de chaque approche.

Tout ce dont nous avons besoin est deprovide an implementation of these two interfaces:

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éthodegetAnyConnection pour obtenir une connexion. Sinon, il utilisera la méthodegetConnection.

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'interfaceDataSource de Java - nous utiliserions l'implémentationDataSourceBasedMultiTenantConnectionProviderImpl

  • En utilisant l'interfaceConnectionProvider d'Hibernate - nous utiliserions l'implémentationAbstractMultiTenantConnectionProvider

3.2. CurrentTenantIdentifierResolver

Il y amany possible ways to resolve a tenant identifier. Par exemple, notre implémentation pourrait utiliser un identifiant de client hébergé défini dans un fichier de configuration.

Une autre solution consiste à utiliser l'identificateur de client hébergé à partir d'un paramètre de chemin.

Voyons cette interface:

public interface CurrentTenantIdentifierResolver {

    String resolveCurrentTenantIdentifier();

    boolean validateExistingCurrentSessions();
}

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

4. Approche de schéma

Dans cette stratégie, nous utiliserons différents schémas ou utilisateurs dans la même instance de base de données physique. This approach should be used when we need the best performance for our application and can sacrifice special database features such as backup per tenant.

De plus, nous nous moquerons de l'interfaceCurrentTenantIdentifierResolver 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'interfaceMultiTenantConnectionProvider seraset the schema to use every time a connection is requested:

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 client.

Configurons leshibernate.properties pour utiliser le mode multitenancy de schéma et notre implémentation de l'interfaceMultiTenantConnectionProvider:

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.example.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, car les schémas doivent déjà être en place.

Pour notre test, nous allons ajouter une entréeCar dans le locatairemyDb1. 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 locatairemyDb2:

@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 changeons le locataire lors de l'appel à la méthodewhenCurrentTenantIs.

5. Approche de base de données

L'approche mutualisée de base de données utilisedifferent physical database instances per tenant. Étant donné que chaque locataire est entièrement isolé,we should choose this strategy when we need special database features like backup per tenant more than we need the best performance.

Pour l'approche Database, nous utiliserons la même classeMultitenancyIntegrationTest et l'interfaceCurrentTenantIdentifierResolver que ci-dessus.

Pour l'interfaceMultiTenantConnectionProvider, nous utiliserons une collectionMap pour obtenir unConnectionProvider par identifiant de client:

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

ChaqueConnectionProvider est renseigné via le fichier de configurationhibernate-database-<tenant identifier>.properties, qui contient tous les détails de connexion:

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

Enfin, mettons à nouveau à jour leshibernate.properties pour utiliser le mode de gestion mutualisée de la base de données et notre implémentation de l'interfaceMultiTenantConnectionProvider:

hibernate.multiTenancy=DATABASE
hibernate.multi_tenant_connection_provider=\
  com.example.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 par Hibernate 5 de la multi-organisation en utilisant des approches de base de données et 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 nosGithub project.