1. 序章

マルチテナンシーを使用すると、複数のクライアントまたはテナントが単一のリソース、またはこの記事のコンテキストでは単一のデータベースインスタンスを使用できます。 目的は、各テナントが必要とする情報を共有データベースから分離することです。

このチュートリアルでは、さまざまなアプローチを紹介します Hibernate5でマルチテナンシーを構成する方法。

2. Mavenの依存関係

pom.xmlファイルにhibernate-core依存関係を含める必要があります。

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-core</artifactId>
   <version>5.2.12.Final</version>
</dependency>

テストには、H2インメモリデータベースを使用するので、この依存関係pom.xmlファイルにも追加しましょう。

<dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <version>1.4.196</version>
</dependency>

3. Hibernateのマルチテナンシーを理解する

公式のHibernateユーザーガイドで述べられているように、Hibernateのマルチテナンシーには3つのアプローチがあります。

  • 個別のスキーマ–同じ物理データベースインスタンス内のテナントごとに1つのスキーマ
  • 個別のデータベース–テナントごとに1つの個別の物理データベースインスタンス
  • パーティション化された(ディスクリミネーター)データ–各テナントのデータはディスクリミネーター値によってパーティション化されます

The パーティション化された(ディスクリミネーター)データアプローチは、Hibernateではまだサポートされていません。 フォローアップこのJIRAの問題将来の進歩のために。

いつものように、Hibernateは各アプローチの実装に関する複雑さを抽象化します。

必要なのは、これら2つのインターフェースの実装を提供することです

データベースとスキーマアプローチの例を実行する前に、各概念をより詳細に見ていきましょう。

3.1. MultiTenantConnectionProvider

基本的に、このインターフェースは具体的なテナント識別子のデータベース接続を提供します。

その2つの主な方法を見てみましょう。

interface MultiTenantConnectionProvider extends Service, Wrapped {
    Connection getAnyConnection() throws SQLException;

    Connection getConnection(String tenantIdentifier) throws SQLException;
     // ...
}

Hibernateが使用するテナント識別子を解決できない場合、HibernateはメソッドgetAnyConnectionを使用して接続を取得します。 それ以外の場合は、メソッドgetConnectionを使用します。

Hibernateは、データベース接続の定義方法に応じて、このインターフェースの2つの実装を提供します。

  • JavaのDataSourceインターフェースを使用– DataSourceBasedMultiTenantConnectionProviderImpl実装を使用します
  • HibernateのConnectionProviderインターフェイスを使用する– AbstractMultiTenantConnectionProvider実装を使用します

3.2. CurrentTenantIdentifierResolver

テナント識別子を解決する方法はたくさんあります。 たとえば、実装では、構成ファイルで定義された1つのテナント識別子を使用できます。

別の方法は、パスパラメータからテナント識別子を使用することです。

このインターフェースを見てみましょう:

public interface CurrentTenantIdentifierResolver {

    String resolveCurrentTenantIdentifier();

    boolean validateExistingCurrentSessions();
}

HibernateはメソッドresolveCurrentTenantIdentifierを呼び出して、テナントIDを取得します。 Hibernateがすべての既存のセッションが同じテナント識別子に属していることを検証する場合、メソッドvalidateExistingCurrentSessionsはtrueを返す必要があります。

4. スキーマアプローチ

この戦略では、同じ物理データベースインスタンスで異なるスキーマまたはユーザーを使用します。 このアプローチは、アプリケーションに最高のパフォーマンスが必要であり、テナントごとのバックアップなどの特別なデータベース機能を犠牲にする可能性がある場合に使用する必要があります。

また、 CurrentTenantIdentifierResolver インターフェイスをモックして、テスト中に選択できる1つのテナント識別子を提供します。

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

MultiTenantConnectionProvider インターフェースの実装は、接続が要求されるたびに使用するスキーマを設定します

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

したがって、テナントごとに1つずつ、2つのスキーマを持つ1つのインメモリH2データベースを使用します。

スキーママルチテナンシーモードとMultiTenantConnectionProvider interface の実装を使用するようにhibernate.propertiesを構成しましょう。

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

テストの目的で、hibernate.connection.urlプロパティを構成して2つのスキーマを作成しました。 スキーマはすでに配置されているはずなので、これは実際のアプリケーションには必要ありません。

テストでは、1つ追加しますテナントへの入場 myDb1。 このエントリがデータベースに保存されており、テナントにないことを確認します myDb2

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

テストでわかるように、whenCurrentTenantIsメソッドを呼び出すときにテナントを変更します。

5. データベースアプローチ

データベースマルチテナンシーアプローチでは、テナントごとに異なる物理データベースインスタンスを使用します。 各テナントは完全に分離されているため、最高のパフォーマンスが必要な場合よりも、テナントごとのバックアップなどの特別なデータベース機能が必要な場合は、この戦略を選択する必要があります。

データベースアプローチでは、上記と同じMultitenancyIntegrationTestクラスとCurrentTenantIdentifierResolverインターフェイスを使用します。

MultiTenantConnectionProvider インターフェイスの場合、 Map コレクションを使用して、テナント識別子ごとにConnectionProviderを取得します。

class MapMultiTenantConnectionProvider
  extends AbstractMultiTenantConnectionProvider {

    private Map<String, ConnectionProvider> 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);
    }
}

ConnectionProvider 構成ファイルを介して入力されます hibernate-データベース- 。プロパティ、 これにはすべての接続の詳細があります:

hibernate.connection.driver_class=org.h2.Driver
hibernate.connection.url=jdbc:h2:mem:<Tenant Identifier>;DB_CLOSE_DELAY=-1
hibernate.connection.username=sa
hibernate.dialect=org.hibernate.dialect.H2Dialect

最後に、 hibernate.properties を再度更新して、データベースマルチテナンシーモードとMultiTenantConnectionProviderインターフェイスの実装を使用します。

hibernate.multiTenancy=DATABASE
hibernate.multi_tenant_connection_provider=\
  com.baeldung.hibernate.multitenancy.database.MapMultiTenantConnectionProvider

スキーマアプローチとまったく同じテストを実行すると、テストは再び合格します。

6. 結論

この記事では、個別のデータベースと個別のスキーマアプローチを使用したマルチテナンシーのHibernate5サポートについて説明します。 これら2つの戦略の違いを調査するために、非常に単純な実装と例を提供します。

この記事で使用されている完全なコードサンプルは、GitHubプロジェクトで入手できます。