Руководство по многопользовательскому режиму в 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 Зависимости

Нам нужно будет включитьthe hibernate-core dependency в файлpom.xml:


   org.hibernate
   hibernate-core
   5.2.12.Final

Для тестирования мы будем использовать базу данных H2 в памяти, поэтому давайте также добавимthis dependency в файлpom.xml:


   com.h2database
   h2
   1.4.196

3. Понимание мультиарендности в Hibernate

Как упоминалось в официальномHibernate User Guide, в Hibernate есть три подхода к мультиарендности:

  • Separate Schema – одна схема для каждого клиента в одном и том же экземпляре физической базы данных

  • Separate Database – один отдельный физический экземпляр базы данных на каждого клиента

  • 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

По сути, этот интерфейс обеспечивает соединение с базой данных для конкретного идентификатора клиента.

Давайте посмотрим на два основных метода:

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

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

Если Hibernate не может разрешить используемый идентификатор клиента, он будет использовать методgetAnyConnection для получения соединения. В противном случае он будет использовать методgetConnection.

Hibernate предоставляет две реализации этого интерфейса в зависимости от того, как мы определяем соединения с базой данных:

  • Используя интерфейсDataSource из Java - мы будем использовать реализациюDataSourceBasedMultiTenantConnectionProviderImpl

  • Используя интерфейсConnectionProvider из Hibernate - мы бы использовали реализациюAbstractMultiTenantConnectionProvider

3.2. CurrentTenantIdentifierResolver

Естьmany possible ways to resolve a tenant identifier. Например, наша реализация может использовать один идентификатор клиента, определенный в файле конфигурации.

Другим способом может быть использование идентификатора клиента из параметра пути.

Посмотрим на этот интерфейс:

public interface CurrentTenantIdentifierResolver {

    String resolveCurrentTenantIdentifier();

    boolean validateExistingCurrentSessions();
}

Hibernate вызывает методresolveCurrentTenantIdentifier для получения идентификатора клиента. Если мы хотим, чтобы 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, чтобы предоставить один идентификатор клиента в качестве нашего выбора во время теста:

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

Итак, мы будем использовать одну базу данных H2 в памяти с двумя схемами - по одной для каждого клиента.

Давайте настроимhibernate.properties для использования режима мультиарендности схемы и нашей реализации интерфейсаMultiTenantConnectionProvider:

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

Для целей нашего теста мы настроили свойствоhibernate.connection.url для создания двух схем. В этом нет необходимости для реального приложения, поскольку схемы уже должны быть на месте.

Для нашего теста мы добавим одну записьCar в тенантmyDb1.. Мы проверим, что эта запись была сохранена в нашей базе данных и что ее нет в клиенте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 для нескольких арендаторов с использованием отдельной базы данных и отдельных схем. Мы предоставляем очень упрощенные реализации и примеры, чтобы исследовать различия между этими двумя стратегиями.

Полные примеры кода, использованные в этой статье, доступны на нашемGithub project.