Автор оригинала: Vlad Mihalcea.
Как я объяснил в этой статье , мультитенантность-это архитектурный шаблон, который позволяет изолировать клиентов, даже если они используют одни и те же аппаратные или программные компоненты.
Существует несколько способов достижения многодетности, и в этой статье мы рассмотрим, как можно реализовать архитектуру многодетности, используя схему базы данных в качестве единицы изоляции.
Многодоместное владение на основе схемы может быть достигнуто с помощью любой системы реляционных баз данных, которая определяет различие между каталогом и схемой. В этой статье мы собираемся использовать PostgreSQL, чтобы продемонстрировать, как мы можем реализовать многоуровневую архитектуру на основе схем с JPA и гибернацией.
Если мы выполняем следующий запрос PostgreSQL в текущем каталоге базы данных:
select nspname as "Schema" from pg_catalog.pg_namespace where nspname not like 'pg_%';
В PostgreSQL будут перечислены следующие схемы баз данных:
Азия |
Европа |
information_schema |
performance_schema |
sys |
Обратите внимание на схемы баз данных азия
и Европа
. Эти схемы являются двумя арендаторами, которые мы собираемся использовать в наших приложениях. Таким образом, если пользователь находится в Европе, он подключится к схеме европа
, а если пользователь находится в Азии, он будет перенаправлен на схему базы данных азия
.
Все арендаторы содержат одни и те же таблицы базы данных. Для нашего примера предположим, что мы используем следующие пользователи
и сообщения
таблицы:
Вышеупомянутые таблицы базы данных могут быть сопоставлены со следующими объектами JPA:
@Entity(name = "User") @Table(name = "users") public class User { @Id @GeneratedValue private Long id; private String firstName; private String lastName; @Column(name = "registered_on") @CreationTimestamp private LocalDateTime createdOn; //Getters and setters omitted for brevity }
@Entity(name = "Post") @Table(name = "posts") public class Post { @Id @GeneratedValue private Long id; private String title; @Column(name = "created_on") @CreationTimestamp private LocalDateTime createdOn; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; //Getters and setters omitted for brevity }
Есть 3 параметра, о которых нам необходимо позаботиться при реализации архитектуры с несколькими арендаторами с помощью Hibernate:
- стратегия множественной аренды
-
Поставщик подключения для нескольких арендаторов
реализация - реализация
CurrentTenantIdentifierResolver
Стратегия гибернации с несколькими арендаторами
Перечисление Hibernate MultiTenancyStrategy
Java используется для указания типа используемой архитектуры с несколькими арендаторами. Для многоквартирного жилья на основе схемы нам нужно использовать стратегию MultiTenancyStrategy.СХЕМА
значение и передайте его через свойство hibernate.multiTenancy
конфигурация:
Реализация Multitenantconnectionprovider
Теперь, чтобы Hibernate правильно перенаправлял запросы на подключение к базе данных в схему базы данных, с которой связан каждый пользователь, нам необходимо предоставить MultiTenancyConnectionProvider
реализацию через hibernate.multitenant_connection_provider
свойство конфигурации:
properties.put( AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, MultiTenantConnectionProvider.INSTANCE );
В нашем примере класс MultiTenantConnectionProvider
выглядит следующим образом:
public class MultiTenantConnectionProvider extends AbstractMultiTenantConnectionProvider { public static final MultiTenantConnectionProvider INSTANCE = new MultiTenantConnectionProvider(); private final MapconnectionProviderMap = new HashMap<>(); Map getConnectionProviderMap() { return connectionProviderMap; } @Override protected ConnectionProvider getAnyConnectionProvider() { return connectionProviderMap.get( TenantContext.DEFAULT_TENANT_IDENTIFIER ); } @Override protected ConnectionProvider selectConnectionProvider( String tenantIdentifier) { return connectionProviderMap.get( tenantIdentifier ); } }
Карта ConnectionProvider
используется для хранения гибернации Поставщика соединений
, связанного с заданным идентификатором арендатора. Спящий режим Поставщик подключений
является фабрикой подключений к базе данных, поэтому каждая схема базы данных будет иметь свой собственный экземпляр ConnectionProvider
.
Чтобы зарегистрировать Поставщика подключения
с помощью нашего MultiTenantConnectionProvider
мы собираемся использовать следующий addTenantConnectionProvider
метод:
private void addTenantConnectionProvider( String tenantId, DataSource tenantDataSource, Properties properties) { DatasourceConnectionProviderImpl connectionProvider = new DatasourceConnectionProviderImpl(); connectionProvider.setDataSource(tenantDataSource); connectionProvider.configure(properties); MultiTenantConnectionProvider.INSTANCE .getConnectionProviderMap() .put( tenantId, connectionProvider ); }
Мы используем JDBC Источник данных
для создания гибернации DatasourceConnectionProviderImpl
, которая дополнительно связана с заданным идентификатором арендатора и хранится в connectionProviderMap
.
Например, мы можем зарегистрировать по умолчанию Источник данных
, который не связан ни с одним таким арендатором:
addTenantConnectionProvider( TenantContext.DEFAULT_TENANT_IDENTIFIER, defaultDataSource, properties() );
По умолчанию Источник данных
будет использоваться Hibernate при загрузке EntityManagerFactory
или всякий раз, когда мы не предоставляем данный идентификатор клиента, что может иметь место для функций администрирования нашей корпоративной системы.
Теперь, чтобы зарегистрировать фактических арендаторов, мы можем использовать следующий метод add Tenant ConnectionProvider
утилита:
private void addTenantConnectionProvider( String tenantId) { PGSimpleDataSource defaultDataSource = (PGSimpleDataSource) database() .dataSourceProvider() .dataSource(); Properties properties = properties(); PGSimpleDataSource tenantDataSource = new PGSimpleDataSource(); tenantDataSource.setDatabaseName(defaultDataSource.getDatabaseName()); tenantDataSource.setCurrentSchema(tenantId); tenantDataSource.setServerName(defaultDataSource.getServerName()); tenantDataSource.setUser(defaultDataSource.getUser()); tenantDataSource.setPassword(defaultDataSource.getPassword()); properties.put( Environment.DATASOURCE, dataSourceProxyType().dataSource(tenantDataSource) ); addTenantConnectionProvider(tenantId, tenantDataSource, properties); }
И наши два жильца будут зарегистрированы вот так:
addTenantConnectionProvider("asia"); addTenantConnectionProvider("europe");
Реализация CurrentTenantIdentifierResolver
Последнее, что нам нужно предоставить для гибернации, – это реализация интерфейса CurrentTenantIdentifierResolver
. Это будет использоваться для поиска идентификатора клиента, связанного с текущим запущенным потоком.
Для нашего приложения реализация CurrentTenantIdentifierResolver
выглядит следующим образом:
public class TenantContext { public static final String DEFAULT_TENANT_IDENTIFIER = "public"; private static final ThreadLocalTENANT_IDENTIFIER = new ThreadLocal<>(); public static void setTenant(String tenantIdentifier) { TENANT_IDENTIFIER.set(tenantIdentifier); } public static void reset(String tenantIdentifier) { TENANT_IDENTIFIER.remove(); } public static class TenantIdentifierResolver implements CurrentTenantIdentifierResolver { @Override public String resolveCurrentTenantIdentifier() { String currentTenantId = TENANT_IDENTIFIER.get(); return currentTenantId != null ? currentTenantId : DEFAULT_TENANT_IDENTIFIER; } @Override public boolean validateExistingCurrentSessions() { return false; } } }
При использовании Spring Контекст Арендатора
может использовать Область запроса
компонент, который предоставляет идентификатор арендатора текущего потока, который был разрешен аспектом AOP до вызова Службы
уровня.
Чтобы предоставить реализацию CurrentTenantIdentifierResolver
для перехода в режим гибернации, необходимо использовать свойство конфигурации hibernate.tenant_identifier_resolver
:
properties.setProperty( AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, TenantContext.TenantIdentifierResolver.class.getName() );
Теперь, при выполнении следующего тестового случая:
TenantContext.setTenant("europe"); User vlad = doInJPA(entityManager -> { LOGGER.info( "Current schema: {}", entityManager.createNativeQuery( "select current_schema()") .getSingleResult() ); User user = new User(); user.setFirstName("Vlad"); user.setLastName("Mihalcea"); entityManager.persist(user); return user; });
Hibernate собирается вставить Пользователя
сущность в европу
арендатора:
INFO [main]: SchemaMultitenancyTest - Current schema: europe LOG: execute: BEGIN LOG: execute : select nextval ('hibernate_sequence') LOG: execute : insert into users ( registered_on, firstName, lastName, id ) values ( $1, $2, $3, $4 ) DETAIL: parameters: $1 = '2018-08-29 09:38:13.042', $2 = 'Vlad', $3 = 'Mihalcea', $4 = '1' LOG: execute S_1: COMMIT
Обратите внимание на идентификатор базы данных европа
в журнале MySQL.
Предполагая, что другой пользователь входит в систему и связан с asia
арендатором:
TenantContext.setTenant("asia");
При сохранении следующего Пользователя
объекта:
doInJPA(entityManager -> { LOGGER.info( "Current schema: {}", entityManager.createNativeQuery( "select current_schema()") .getSingleResult() ); User user = new User(); user.setFirstName("John"); user.setLastName("Doe"); entityManager.persist(user); });
Hibernate вставит его в схему базы данных азия
:
INFO [main]: SchemaMultitenancyTest - Current schema: asia LOG: execute: BEGIN LOG: execute : select nextval ('hibernate_sequence') LOG: execute : insert into users ( registered_on, firstName, lastName, id ) values ( $1, $2, $3, $4 ) DETAIL: parameters: $1 = '2018-08-29 09:39:52.448', $2 = 'John', $3 = 'Doe', $4 = '1' LOG: execute S_1: COMMIT
При переключении обратно на европу
арендатора и сохранении Записи
сущности, связанной с владом
| Пользователем сущностью, которую мы ранее сохранили в базе данных:
TenantContext.setTenant("europe"); doInJPA(entityManager -> { LOGGER.info( "Current schema: {}", entityManager.createNativeQuery( "select current_schema()") .getSingleResult() ); Post post = new Post(); post.setTitle("High-Performance Java Persistence"); post.setUser(vlad); entityManager.persist(post); });
Hibernate выполнит инструкции для схемы базы данных европа
:
INFO [main]: SchemaMultitenancyTest - Current schema: europe LOG: execute: BEGIN LOG: execute : select nextval ('hibernate_sequence') LOG: execute : insert into users ( registered_on, firstName, lastName, id ) values ( $1, $2, $3, $4 ) DETAIL: parameters: $1 = '2018-08-29 09:43:00.683', $2 = 'High-Performance Java Persistence', $3 = '1', $4 = '2' LOG: execute S_1: COMMIT
Круто, правда?
Реализация архитектуры с несколькими арендаторами с помощью Hibernate довольно проста, но очень мощна. Стратегия множественного найма на основе схемы очень подходит для систем баз данных, которые проводят четкое различие между каталогом базы данных и схемой, например PostgreSQL.