Автор оригинала: Vlad Mihalcea.
Как я объяснил в этой статье , мультитенантность-это архитектурный шаблон, который позволяет изолировать клиентов, даже если они используют одни и те же аппаратные или программные компоненты.
Существует несколько способов достижения многодетности, и в этой статье мы рассмотрим, как можно реализовать архитектуру многодетности, используя каталог базы данных в качестве единицы изоляции.
Многопользовательская аренда на основе каталога может быть достигнута с использованием любой системы реляционных баз данных. Однако, поскольку MySQL является одной из самых популярных СУБД и поскольку она не проводит реального различия между каталогом и схемой, в этой статье мы собираемся использовать MySQL, чтобы продемонстрировать, как мы можем реализовать многозадачную архитектуру на основе каталога с помощью JPA и Hibernate.
Итак, если мы запустим команду показать базы данных
в MySQL, мы получим следующие результаты:
Азия |
Европа |
information_schema |
performance_schema |
sys |
Обратите внимание на каталоги баз данных азия
и Европа
. Эти каталоги-два арендатора, которых мы собираемся использовать в наших приложениях. Таким образом, если пользователь находится в Европе, он подключится к базе данных европа
, а если пользователь находится в Азии, он будет перенаправлен в каталог базы данных азия|/.
Все арендаторы содержат одни и те же таблицы базы данных. Для нашего примера предположим, что мы используем следующие пользователи
и сообщения
таблицы:
Вышеупомянутые таблицы базы данных могут быть сопоставлены со следующими объектами JPA:
@Entity(name = "User") @Table(name = "users") public class User { @Id @GeneratedValue( strategy = GenerationType.IDENTITY ) 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( strategy = GenerationType.IDENTITY ) 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
используется для хранения гибернации Поставщика соединений
, связанного с заданным идентификатором арендатора. Hibernate Поставщик подключений
является фабрикой подключений к базе данных, поэтому каждый каталог баз данных будет иметь свой собственный экземпляр 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) { DataSourceProvider dataSourceProvider = database() .dataSourceProvider(); Properties properties = properties(); MysqlDataSource tenantDataSource = new MysqlDataSource(); tenantDataSource.setDatabaseName(tenantId); tenantDataSource.setUser(dataSourceProvider.username()); tenantDataSource.setPassword(dataSourceProvider.password()); 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 -> { User user = new User(); user.setFirstName("Vlad"); user.setLastName("Mihalcea"); entityManager.persist(user); return user; });
Hibernate собирается вставить Пользователя
сущность в европу
арендатора:
Connect mysql@localhost on europe using TCP/IP Query SET character_set_results = NULL Query SET autocommit=1 Query SET autocommit=0 Query insert into users (registered_on, firstName, lastName) values ('2018-08-16 09:55:08.71', 'Vlad', 'Mihalcea') Query select last_insert_id() Query commit Query SET autocommit=1 Quit
Обратите внимание на идентификатор базы данных европа
в журнале MySQL.
Предполагая, что другой пользователь входит в систему и связан с asia
арендатором:
TenantContext.setTenant("asia");
При сохранении следующего Пользователя
объекта:
doInJPA(entityManager -> { User user = new User(); user.setFirstName("John"); user.setLastName("Doe"); entityManager.persist(user); });
Hibernate вставит его в каталог базы данных азия
:
Connect mysql@localhost on asia using TCP/IP Query SET character_set_results = NULL Query SET autocommit=1 Query SET autocommit=0 Query insert into users (registered_on, firstName, lastName) values ('2018-08-16 09:59:35.763', 'John', 'Doe') Query select last_insert_id() Query commit Query SET autocommit=1 Quit
При переключении обратно на европу
арендатора и сохранении Записи
сущности, связанной с владом
| Пользователем сущностью, которую мы ранее сохранили в базе данных:
TenantContext.setTenant("europe"); doInJPA(entityManager -> { Post post = new Post(); post.setTitle("High-Performance Java Persistence"); post.setUser(vlad); entityManager.persist(post); });
Hibernate выполнит инструкции для каталога базы данных европа
:
Connect mysql@localhost on europe using TCP/IP Query SET character_set_results = NULL Query SET autocommit=1 Query SET autocommit=0 Query insert into posts (created_on, title, user_id) values ('2018-08-16 10:02:15.408', 'High-Performance Java Persistence', 1) Query select last_insert_id() Query commit Query SET autocommit=1 Quit
Круто, правда?
Реализация архитектуры с несколькими арендаторами с помощью Hibernate довольно проста, но очень мощна. Стратегия многоквартирного проживания на основе каталога очень подходит для систем баз данных, которые не проводят четкого различия между каталогом базы данных и схемой, например MySQL или MariaDB.