Hibernate 5のマルチテナンシーガイド

Hibernate 5のマルチテナンシーガイド

1. 前書き

Multitenancyを使用すると、複数のクライアントまたはテナントが単一のリソース、またはこの記事のコンテキストでは単一のデータベースインスタンスを使用できます。 目的はto isolate the information each tenant needs from the shared databaseです。

このチュートリアルでは、さまざまなアプローチを紹介しますto configuring multitenancy in Hibernate 5.

2. Mavenの依存関係

pom.xmlファイルにthe hibernate-core dependencyを含める必要があります。


   org.hibernate
   hibernate-core
   5.2.12.Final

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


   com.h2database
   h2
   1.4.196

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

公式のHibernate User Guideで述べたように、Hibernateのマルチテナンシーには3つのアプローチがあります。

  • Separate Schema –同じ物理データベースインスタンス内のテナントごとに1つのスキーマ

  • Separate Database –テナントごとに1つの個別の物理データベースインスタンス

  • Partitioned (Discriminator) Data –各テナントのデータはディスクリミネーター値で分割されます

Partitioned (Discriminator) Data approach isn’t yet supported by Hibernate.将来の進歩のためにthis JIRA issueをフォローアップします。

通常どおり、Hibernateは各アプローチの実装に関する複雑さを抽象化します。

必要なのはprovide an implementation of these two interfacesです。

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

3.1. MultiTenantConnectionProvider

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

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

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

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

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

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

  • JavaからのDataSourceインターフェースの使用–DataSourceBasedMultiTenantConnectionProviderImpl実装を使用します

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

3.2. CurrentTenantIdentifierResolver

many possible ways to resolve a tenant identifierがあります。 たとえば、実装では、構成ファイルで定義された1つのテナント識別子を使用できます。

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

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

public interface CurrentTenantIdentifierResolver {

    String resolveCurrentTenantIdentifier();

    boolean validateExistingCurrentSessions();
}

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

4. スキーマアプローチ

この戦略では、同じ物理データベースインスタンスで異なるスキーマまたはユーザーを使用します。 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.

また、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インターフェースの実装はset 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;
    }
}

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

スキーママルチテナンシーモードとMultiTenantConnectionProviderインターフェイス:の実装を使用するように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.example.hibernate.multitenancy.schema.SchemaMultiTenantConnectionProvider

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

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

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

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

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

データベースマルチテナンシーアプローチはdifferent physical database instances per tenantを使用します。 各テナントは完全に分離されているため、we should choose this strategy when we need special database features like backup per tenant more than we need the best performance.

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

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

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

ConnectionProviderは、すべての接続の詳細を含む構成ファイルhibernate-database-<tenant identifier>.properties,を介して入力されます。

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

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

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

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

6. 結論

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

この記事で使用されている完全なコードサンプルは、Github projectで入手できます。