Рубрики
Без рубрики

Гибернация каталога баз данных в нескольких арендаторах

Узнайте, как реализовать архитектуру многопользовательской базы данных на основе каталога при использовании JPA и гибернации.

Автор оригинала: 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 Map connectionProviderMap = 
            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 ThreadLocal TENANT_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.