Ein Leitfaden für die Mandantenfähigkeit im Ruhezustand 5
1. Einführung
Multitenancy ermöglicht es mehreren Clients oder Mandanten, eine einzelne Ressource oder im Kontext dieses Artikels eine einzelne Datenbankinstanz zu verwenden. Der Zweck istto isolate the information each tenant needs from the shared database.
In diesem Tutorial werden verschiedene Ansätzeto configuring multitenancy in Hibernate 5. vorgestellt
2. Maven-Abhängigkeiten
Wir müssenthe hibernate-core dependency in diepom.xml-Datei aufnehmen:
org.hibernate
hibernate-core
5.2.12.Final
Zum Testen verwenden wir eine H2-In-Memory-Datenbank. Fügen wir also auchthis dependency zurpom.xml-Datei hinzu:
com.h2database
h2
1.4.196
3. Multitenancy im Ruhezustand verstehen
Wie in den offiziellenHibernate User Guide erwähnt, gibt es im Ruhezustand drei Ansätze für die Mandantenfähigkeit:
-
Separate Schema – ein Schema pro Mandant in derselben physischen Datenbankinstanz
-
Separate Database –ist eine separate physische Datenbankinstanz pro Mandant
-
Partitioned (Discriminator) Data – Die Daten für jeden Mandanten werden durch einen Diskriminatorwert partitioniert
Partitioned (Discriminator) Data approach isn’t yet supported by Hibernate. Follow-up vonthis JIRA issue für zukünftige Fortschritte.
Wie üblich abstrahiert Hibernate die Komplexität bei der Implementierung der einzelnen Ansätze.
Alles was wir brauchen istprovide an implementation of these two interfaces:
-
MultiTenantConnectionProvider - stellt Verbindungen pro Mandant bereit
-
CurrentTenantIdentifierResolver - Löst die zu verwendende Mandantenkennung auf
Sehen wir uns jedes Konzept genauer an, 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 Mandantenidentifikation.
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 MethodegetAnyConnection verwendet, um eine Verbindung herzustellen. Andernfalls wird die MethodegetConnection verwendet.
Hibernate bietet zwei Implementierungen dieser Schnittstelle, je nachdem, wie wir die Datenbankverbindungen definieren:
-
Verwenden Sie dieDataSource-Schnittstelle von Java - wir würden dieDataSourceBasedMultiTenantConnectionProviderImpl-Implementierung verwenden
-
Unter Verwendung derConnectionProvider-Schnittstelle aus dem Ruhezustand würden wir dieAbstractMultiTenantConnectionProvider-Implementierung verwenden
3.2. CurrentTenantIdentifierResolver
Es gibtmany possible ways to resolve a tenant identifier. Beispielsweise könnte unsere Implementierung eine Mandanten-ID verwenden, die in einer Konfigurationsdatei definiert ist.
Eine andere Möglichkeit besteht darin, die Mandanten-ID aus einem Pfadparameter zu verwenden.
Sehen wir uns diese Oberfläche an:
public interface CurrentTenantIdentifierResolver {
String resolveCurrentTenantIdentifier();
boolean validateExistingCurrentSessions();
}
Hibernate ruft die MethoderesolveCurrentTenantIdentifier auf, um die Mandantenkennung abzurufen. Wenn im Ruhezustand überprüft werden soll, ob alle vorhandenen Sitzungen zur selben Mandanten-ID gehören, sollte die MethodevalidateExistingCurrentSessions true zurückgeben.
4. Schema-Ansatz
In dieser Strategie verwenden wir unterschiedliche Schemas oder Benutzer in derselben physischen Datenbankinstanz. 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.
Außerdem verspotten wir dieCurrentTenantIdentifierResolver-Schnittstelle, um während des Tests eine Mandantenkennung 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();
}
}
Unsere Implementierung derMultiTenantConnectionProvider-Schnittstelle wirdset 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;
}
}
Daher verwenden wir eine In-Memory-H2-Datenbank mit zwei Schemas - eines pro Mandant.
Konfigurieren Sie diehibernate.properties so, dass sie den Schema-Mandantenfähigkeitsmodus und unsere Implementierung derMultiTenantConnectionProvider-Schnittstelle: verwenden
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
Für unseren Test haben wir die Eigenschafthibernate.connection.urlo konfiguriert, dass zwei Schemas erstellt werden. Dies sollte für eine echte Anwendung nicht erforderlich sein, da die Schemas bereits vorhanden sein sollten.
Für unseren Test fügen wir einenCar-Eintrag in den MandantenmyDb1.ein. Wir überprüfen, ob dieser Eintrag in unserer Datenbank gespeichert wurde und nicht in dem MandantenmyDb2:
@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, wenn wir die MethodewhenCurrentTenantIsaufrufen.
5. Datenbankansatz
Der Datenbank-Mandantenfähigkeitsansatz verwendetdifferent physical database instances per tenant. Da jeder Mieter vollständig isoliert ist,we should choose this strategy when we need special database features like backup per tenant more than we need the best performance.
Für den Datenbankansatz verwenden wir dieselbeMultitenancyIntegrationTest-Klasse und dieselbeCurrentTenantIdentifierResolver-Schnittstelle wie oben.
Für dieMultiTenantConnectionProvider-Schnittstelle verwenden wir eineMap-Sammlung, um eineConnectionProvider pro Mandanten-ID zu erhalten:
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);
}
}
JedesConnectionProvider wird über die Konfigurationsdateihibernate-database-<tenant identifier>.properties, ausgefüllt, die alle Verbindungsdetails enthält:
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
Zum Schluss aktualisieren wir diehibernate.properties erneut, um den Datenbank-Mandantenfähigkeitsmodus und unsere Implementierung derMultiTenantConnectionProvider-Schnittstelle zu verwenden:
hibernate.multiTenancy=DATABASE
hibernate.multi_tenant_connection_provider=\
com.example.hibernate.multitenancy.database.MapMultiTenantConnectionProvider
Wenn wir den exakt gleichen Test wie im Schema-Ansatz ausführen, wird der Test erneut bestanden.
6. Fazit
Dieser Artikel behandelt die Unterstützung von Hibernate 5 für die Mandantenfähigkeit mithilfe der getrennten Datenbank und der getrennten 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 auf unserenGithub project verfügbar.