Перейти к основному содержимому

Руководство по мультиарендности в Hibernate 5

· 6 мин. чтения

1. Введение

Мультиарендность позволяет нескольким клиентам или арендаторам использовать один ресурс или, в контексте этой статьи, один экземпляр базы данных. Цель состоит в том, чтобы изолировать информацию, необходимую каждому арендатору, из общей базы данных .

В этом руководстве мы познакомим вас с различными подходами к настройке мультиарендности в Hibernate 5.

2. Зависимости Maven

Нам нужно включить зависимость hibernate-core в файл pom.xml :

<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 существует три подхода к мультиарендности:

  • Отдельная схема — одна схема для каждого арендатора в одном и том же физическом экземпляре базы данных.
  • Отдельная база данных — один отдельный экземпляр физической базы данных на каждого арендатора.
  • Разделенные (дискриминаторные) данные — данные для каждого арендатора секционируются по значению дискриминатора.

Подход с секционированными (дискриминаторными) данными пока не поддерживается Hibernate. Следите за этой проблемой JIRA для будущего прогресса.

Как обычно, Hibernate абстрагируется от сложности реализации каждого подхода.

Все, что нам нужно, это предоставить реализацию этих двух интерфейсов :

MultiTenantConnectionProvider — обеспечивает соединения для каждого арендатора

CurrentTenantIdentifierResolver — разрешает используемый идентификатор арендатора.

Давайте рассмотрим более подробно каждую концепцию, прежде чем переходить к примерам подходов к базе данных и схеме.

3.1. Мультитенантконнектионпровидер

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

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

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

Существует множество возможных способов разрешения идентификатора арендатора . Например, наша реализация может использовать один идентификатор арендатора, определенный в файле конфигурации.

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

Давайте посмотрим на этот интерфейс:

public interface CurrentTenantIdentifierResolver {

String resolveCurrentTenantIdentifier();

boolean validateExistingCurrentSessions();
}

Hibernate вызывает метод resolveCurrentTenantIdentifier для получения идентификатора арендатора. Если мы хотим, чтобы Hibernate проверял, принадлежат ли все существующие сеансы одному и тому же идентификатору арендатора, метод validateExistingCurrentSessions должен возвращать значение true.

4. Схематический подход

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

Кроме того, мы смоделируем интерфейс 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 установит схему для использования каждый раз, когда запрашивается соединение :

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.foreach.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. Подход к базе данных

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

Для подхода с базой данных мы будем использовать тот же класс 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-database-<идентификатор арендатора>.properties, в котором есть все детали подключения:

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.foreach.hibernate.multitenancy.database.MapMultiTenantConnectionProvider

Если мы запустим тот же самый тест, что и в подходе со схемой, тест снова пройдет.

6. Заключение

В этой статье рассматривается поддержка Hibernate 5 для мультиарендности с использованием отдельной базы данных и отдельных подходов к схеме. Мы предоставляем очень упрощенные реализации и примеры, чтобы исследовать различия между этими двумя стратегиями.

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