Ein Leitfaden für die Multitenancy im Ruhezustand 5

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:

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.