Una guida alla multi-tenancy in Hibernate 5

1. Introduzione

La multitenancy consente a più client o tenant di utilizzare una singola risorsa o, nel contesto di questo articolo, una singola istanza di database. Lo scopo è isolare le informazioni di cui ogni tenant ha bisogno dal database condiviso .

In questo tutorial, introdurremo vari approcci alla configurazione della multi-tenancy in Hibernate 5.

2. Dipendenze di Maven

Dovremo includere la dipendenza hibernate-core nel file pom.xml :

 org.hibernate hibernate-core 5.2.12.Final 

Per i test, useremo un database in memoria H2, quindi aggiungiamo anche questa dipendenza al file pom.xml :

 com.h2database h2 1.4.196 

3. Capire Multitenancy in Hibernate

Come menzionato nella guida utente ufficiale di Hibernate, ci sono tre approcci al multi-tenancy in Hibernate:

  • Schema separato: uno schema per tenant nella stessa istanza di database fisica
  • Database separato: un'istanza di database fisica separata per tenant
  • Dati partizionati (discriminatore): i dati per ogni tenant vengono partizionati in base a un valore discriminatore

L' approccio Partitioned (Discriminator) Data non è ancora supportato da Hibernate. Follow-up su questo problema JIRA per progressi futuri.

Come al solito, Hibernate astrae la complessità attorno all'implementazione di ciascun approccio.

Tutto ciò di cui abbiamo bisogno è fornire un'implementazione di queste due interfacce :

  • MultiTenantConnectionProvider : fornisce connessioni per tenant

  • CurrentTenantIdentifierResolver : risolve l'identificatore del tenant da utilizzare

Vediamo più in dettaglio ogni concetto prima di passare attraverso il database e gli esempi di approcci allo schema.

3.1. MultiTenantConnectionProvider

Fondamentalmente, questa interfaccia fornisce una connessione al database per un identificatore concreto del tenant.

Vediamo i suoi due metodi principali:

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

Se Hibernate non è in grado di risolvere l'identificativo del tenant da utilizzare, utilizzerà il metodo getAnyConnection per ottenere una connessione. Altrimenti, utilizzerà il metodo getConnection .

Hibernate fornisce due implementazioni di questa interfaccia a seconda di come definiamo le connessioni al database:

  • Utilizzando l'interfaccia DataSource da Java, utilizzeremo l' implementazione DataSourceBasedMultiTenantConnectionProviderImpl
  • Utilizzando l' interfaccia ConnectionProvider di Hibernate, utilizzeremo l' implementazione di AbstractMultiTenantConnectionProvider

3.2. CurrentTenantIdentifierResolver

Esistono molti modi possibili per risolvere un identificatore tenant . Ad esempio, la nostra implementazione potrebbe utilizzare un identificatore tenant definito in un file di configurazione.

Un altro modo potrebbe essere quello di utilizzare l'identificatore del tenant da un parametro del percorso.

Vediamo questa interfaccia:

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

Hibernate chiama il metodo resolentCurrentTenantIdentifier per ottenere l'identificatore del tenant. Se vogliamo che Hibernate convalidi tutte le sessioni esistenti appartengono allo stesso identificatore tenant, il metodo validateExistingCurrentSessions dovrebbe restituire true.

4. Schema Approach

In questa strategia, utilizzeremo diversi schemi o utenti nella stessa istanza di database fisica. Questo approccio dovrebbe essere utilizzato quando abbiamo bisogno delle migliori prestazioni per la nostra applicazione e possiamo sacrificare funzionalità di database speciali come il backup per tenant.

Inoltre, derideremo l' interfaccia CurrentTenantIdentifierResolver per fornire un identificatore tenant come nostra scelta durante il 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(); } }

La nostra implementazione dell'interfaccia MultiTenantConnectionProvider imposterà lo schema da utilizzare ogni volta che viene richiesta una connessione :

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

Quindi, useremo un database H2 in memoria con due schemi, uno per ogni tenant.

Configuriamo hibernate.properties per utilizzare la modalità multitenancy dello schema e la nostra implementazione dell'interfaccia 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

Ai fini del nostro test, abbiamo configurato la proprietà hibernate.connection.url per creare due schemi. Questo non dovrebbe essere necessario per un'applicazione reale poiché gli schemi dovrebbero essere già presenti.

Per il nostro test, aggiungeremo una voce Car nel tenant myDb1. Verificheremo che questa voce sia stata archiviata nel nostro database e che non sia nel tenant myDb2 :

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

Come possiamo vedere nel test, cambiamo il tenant quando chiamiamo il metodo whenCurrentTenantIs .

5. Approccio database

L'approccio multi-tenancy del database utilizza istanze di database fisiche diverse per tenant . Dal momento che ogni tenant è completamente isolato, dovremmo scegliere questa strategia quando abbiamo bisogno di funzioni di database speciali come il backup per tenant più di quanto abbiamo bisogno delle migliori prestazioni.

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

Se eseguiamo lo stesso identico test dell'approccio schema, il test viene superato di nuovo.

6. Conclusione

Questo articolo copre il supporto di Hibernate 5 per multi-tenancy utilizzando il database separato e approcci di schemi separati. Forniamo implementazioni ed esempi molto semplicistici per sondare le differenze tra queste due strategie.

Gli esempi di codice completo utilizzati in questo articolo sono disponibili nel nostro progetto GitHub.