Ein Leitfaden zur Mandantenfähigkeit im Ruhezustand 5

1. Einleitung

Mit der Mandantenfähigkeit können mehrere Clients oder Mandanten eine einzelne Ressource oder im Kontext dieses Artikels eine einzelne Datenbankinstanz verwenden. Der Zweck besteht darin, die Informationen, die jeder Mandant benötigt, von der gemeinsam genutzten Datenbank zu isolieren .

In diesem Tutorial werden verschiedene Ansätze zum Konfigurieren der Mandantenfähigkeit in Hibernate 5 vorgestellt.

2. Maven-Abhängigkeiten

Wir müssen die Hibernate-Core- Abhängigkeit in die Datei pom.xml aufnehmen :

 org.hibernate hibernate-core 5.2.12.Final 

Zum Testen verwenden wir eine H2-In-Memory-Datenbank. Fügen wir diese Abhängigkeit also auch der Datei pom.xml hinzu :

 com.h2database h2 1.4.196 

3. Multitenancy im Ruhezustand verstehen

Wie im offiziellen Hibernate-Benutzerhandbuch erwähnt, gibt es im Hibernate drei Ansätze für die Mandantenfähigkeit:

  • Separates Schema - Ein Schema pro Mandant in derselben physischen Datenbankinstanz
  • Separate Datenbank - Eine separate physische Datenbankinstanz pro Mandant
  • Partitionierte (Diskriminator-) Daten - Die Daten für jeden Mandanten werden durch einen Diskriminatorwert partitioniert

Der Ansatz für partitionierte (Diskriminator-) Daten wird von Hibernate noch nicht unterstützt. Verfolgen Sie dieses JIRA-Problem für zukünftige Fortschritte.

Wie üblich abstrahiert Hibernate die Komplexität bei der Implementierung jedes Ansatzes.

Wir müssen lediglich eine Implementierung dieser beiden Schnittstellen bereitstellen :

  • MultiTenantConnectionProvider - stellt Verbindungen pro Mandant bereit

  • CurrentTenantIdentifierResolver - Löst die zu verwendende Mandanten-ID auf

Lassen Sie uns jedes Konzept genauer betrachten, bevor wir die Beispiele für Datenbank- und Schemaansätze durchgehen.

3.1. MultiTenantConnectionProvider

Grundsätzlich bietet diese Schnittstelle eine Datenbankverbindung für eine konkrete Mandantenkennung.

Sehen wir uns die beiden Hauptmethoden an:

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

Wenn der Ruhezustand die zu verwendende Mandanten-ID nicht auflösen kann, wird die Methode getAnyConnection verwendet , um eine Verbindung herzustellen. Andernfalls wird die Methode getConnection verwendet .

Hibernate bietet zwei Implementierungen dieser Schnittstelle, je nachdem, wie wir die Datenbankverbindungen definieren:

  • Verwenden der DataSource-Schnittstelle von Java - Wir würden die Implementierung von DataSourceBasedMultiTenantConnectionProviderImpl verwenden
  • Verwenden der ConnectionProvider- Schnittstelle von Hibernate - Wir würden die AbstractMultiTenantConnectionProvider- Implementierung verwenden

3.2. CurrentTenantIdentifierResolver

Es gibt viele Möglichkeiten, eine Mandantenkennung aufzulösen . Unsere Implementierung könnte beispielsweise eine Mandanten-ID verwenden, die in einer Konfigurationsdatei definiert ist.

Eine andere Möglichkeit könnte darin bestehen, die Mandantenkennung aus einem Pfadparameter zu verwenden.

Sehen wir uns diese Schnittstelle an:

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

Hibernate ruft die Methode resolveCurrentTenantIdentifier auf, um die Mandanten- ID abzurufen . Wenn Hibernate überprüfen soll, ob alle vorhandenen Sitzungen zur selben Mandanten-ID gehören, sollte die Methode validateExistingCurrentSessions true zurückgeben.

4. Schema-Ansatz

In dieser Strategie verwenden wir verschiedene Schemas oder Benutzer in derselben physischen Datenbankinstanz. Dieser Ansatz sollte verwendet werden, wenn wir die beste Leistung für unsere Anwendung benötigen und spezielle Datenbankfunktionen wie die Sicherung pro Mandant opfern können.

Außerdem werden wir die CurrentTenantIdentifierResolver- Schnittstelle verspotten , um während des Tests eine Mandanten-ID als unsere Wahl bereitzustellen:

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

Durch unsere Implementierung der MultiTenantConnectionProvider- Schnittstelle wird das Schema so festgelegt, dass es jedes Mal verwendet wird, wenn eine Verbindung angefordert wird :

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

Wir verwenden also eine In-Memory-H2-Datenbank mit zwei Schemas - eines pro Mandant.

Lassen Sie uns konfigurieren Sie die hibernate.properties das Schema Multitenancy Modus und unsere Umsetzung der verwenden MultiTenantConnectionProvider Schnittstelle :

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

Für die Zwecke unserer Prüfung haben wir die konfigurierte hibernate.connection.url Eigenschaft erstellen zwei Schemas. Dies sollte für eine echte Anwendung nicht erforderlich sein, da die Schemas bereits vorhanden sein sollten.

Für unseren Test werden wir ein hinzufügen Auto Eintrag in dem Mieter MYDB1. Wir werden überprüfen, ob dieser Eintrag in unserer Datenbank gespeichert wurde und nicht im Mandanten myDb2 :

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

Wie wir im Test sehen können, ändern wir den Mandanten beim Aufruf der whenCurrentTenantIs- Methode.

5. Datenbankansatz

Der Ansatz der Datenbank-Mandantenfähigkeit verwendet unterschiedliche physische Datenbankinstanzen pro Mandant . Da jeder Mandant vollständig isoliert ist, sollten wir diese Strategie wählen, wenn wir spezielle Datenbankfunktionen wie die Sicherung pro Mandant mehr benötigen als die beste Leistung.

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

Wenn wir genau den gleichen Test wie im Schema-Ansatz ausführen, besteht der Test erneut.

6. Fazit

Dieser Artikel behandelt die Hibernate 5-Unterstützung für Mandantenfähigkeit mithilfe der separaten Datenbank- und separaten Schemaansätze. Wir bieten sehr vereinfachte Implementierungen und Beispiele, um die Unterschiede zwischen diesen beiden Strategien zu untersuchen.

Die vollständigen Codebeispiele, die in diesem Artikel verwendet werden, sind in unserem GitHub-Projekt verfügbar.