Спящий Режим, Мультитенантность И Автоматическое Обновление Схемы Базы Данных

Хотите воспользоваться преимуществами (и недостатками) автоматического обновления схемы базы данных при использовании Hibernate, но иметь мультитенантную архитектуру? Добро пожаловать в кот.



в чем именно проблема?:

Если ваше приложение использует Hibernate и вам необходимо обеспечить автоматическое обновление схемы базы данных при использовании многотенантной архитектуры со стратегией «схема для каждого арендатора», вы не можете просто включить опцию автоматического обновления схемы:
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

<property name="hbm2ddl.auto">update</property>

Давайте разберемся, почему.

Предполагается, что читатель имеет представление о том, как работает мультитенантность в Hibernate.

Конфигурация

БД: Постгрес 9.2. ORM: Hibernate 4.3.7.Final + Envers Стратегия мультитенантности: схема для каждого арендатора с общим источником данных JDBC. Основные настройки гибернации:

<!--<property name="hbm2ddl.auto">update</property>--> <property name="connection.datasource">java:/portalDS</property> <property name="hibernate.dialect">by.tychina.PostgreSQL9MultiTenantDialect</property> <property name="hibernate.multiTenancy">SCHEMA</property> <property name="hibernate.multi_tenant_connection_provider">by.tychina.tenant.HibernateMultiTenantConnectionProvider</property> <property name="hibernate.tenant_identifier_resolver">by.tychina.tenant.HibernateTenantIdentifierResolver</property> <property name="org.hibernate.envers.audit_strategy">org.hibernate.envers.strategy.ValidityAuditStrategy</property>



Вспомогательные классы

Класс TenantId используется для хранения идентификатора арендатора:

package by.tychina.tenant; /** * @author Sergey Tychina */ public class TenantId { /** * Represents database schema name */ private String tenantId; public TenantId(String tenantId) { this.tenantId = tenantId; } public String getTenantId() { return tenantId; } }

, где поле tenantId — это имя схемы в базе данных.

Класс Persistence отвечает за инициализацию конфигурации Hibernate и инициализацию SessionFactory:

package by.tychina; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; /** * @author Sergey Tychina */ public class Persistence { private final Configuration configuration; private final SessionFactory sessionFactory; private static Persistence instance = null; private Persistence() { configuration = new Configuration(); configuration.configure(); sessionFactory = configuration.buildSessionFactory(); } public static synchronized Persistence getInstance() { if (instance == null) { instance = new Persistence(); } return instance; } public Configuration getConfiguration() { return configuration; } public SessionFactory getSessionFactory() { return sessionFactory; } }

Класс ConnectionProvider, необходимый для открытия соединения с базой данных с использованием источника данных:

package by.tychina; import by.tychina.tenant.TenantId; import javax.naming.InitialContext; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; /** * @author Sergey Tychina */ public class ConnectionProvider { private static final String DATASOURCE_JNDI_NAME = "java:/portalDS"; private static DataSource dataSource = null; private static synchronized DataSource getDataSource() { if (dataSource == null) { try { dataSource = (DataSource) new InitialContext().

lookup(DATASOURCE_JNDI_NAME); } catch (Exception e) { throw new RuntimeException(e); } } return dataSource; } public static Connection getConnection() { try { return getDataSource().

getConnection(); } catch (SQLException e) { throw new RuntimeException("Unable to get connection", e); } } public static Connection getConnection(TenantId tenantId) { Connection connection = getConnection(); try { connection.createStatement().

execute("SET SCHEMA '" + tenantId.getTenantId() + "'"); } catch (SQLException e) { try { connection.close(); } catch (SQLException e1) { e1.printStackTrace(); } throw new RuntimeException("Could not alter connection to '" + tenantId.getTenantId() + "' schema", e); } return connection; } }

Hibernate нужен класс, который будет открывать соединение с базой данных с указанным арендатором и без него (опция hibernate.multi_tenant_connection_provider):

package by.tychina.tenant; import by.tychina.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import java.sql.Connection; import java.sql.SQLException; /** * @author Sergey Tychina */ public class HibernateMultiTenantConnectionProvider implements MultiTenantConnectionProvider { @Override public Connection getAnyConnection() throws SQLException { return ConnectionProvider.getConnection(); } @Override public void releaseAnyConnection(Connection connection) throws SQLException { connection.close(); } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { return ConnectionProvider.getConnection(new TenantId(tenantIdentifier)); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { releaseAnyConnection(connection); } @Override public boolean supportsAggressiveRelease() { return false; } @Override public boolean isUnwrappableAs(Class unwrapType) { return false; } @Override public <T> T unwrap(Class<T> unwrapType) { return null; } }

Hibernate нужен класс для определения текущего арендатора (опция hibernate.tenant_identifier_resolver):

package by.tychina.tenant; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; /** * @author Sergey Tychina */ public class HibernateTenantIdentifierResolver implements CurrentTenantIdentifierResolver { @Override public String resolveCurrentTenantIdentifier() { // Put some logic here to get current transaction tenant id // Use ThreadLocal for example to store TenantId when starting a transaction TenantId transactionTenantId = null; return transactionTenantId.getTenantId(); } @Override public boolean validateExistingCurrentSessions() { return true; } }



Диалект Postres для поддержки мультиарендности

Если сопоставления классов используют последовательность для генерации идентификатора, стандартный PostgreSQL9Dialect, предоставляемый Hibernate, не позволит вам правильно генерировать сценарии обновления для создания последовательности.

Проблема заключается в следующем методе класса org.hibernate.dialect.PostgreSQL81Dialect:

public String getQuerySequencesString() { return "select relname from pg_class where relkind='S'"; }

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

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

Созданный нами диалект решает эту проблему:

package by.tychina; import org.hibernate.dialect.PostgreSQL9Dialect; /** * @author Sergey Tychina */ public class PostgreSQL9MultiTenantDialect extends PostgreSQL9Dialect { /** * Query to get sequences for current schema ONLY. Original method returns query to get all sequences (in all schemas) * that breaks multi-tenancy. */ @Override public String getQuerySequencesString() { return "SELECT c.relname FROM pg_catalog.pg_class c " + "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace " + "WHERE c.relkind IN ('S','') " + "AND n.nspname <> 'pg_catalog' " + "AND n.nspname <> 'information_schema' " + "AND n.nspname !~ '^pg_toast' " + "AND pg_catalog.pg_table_is_visible(c.oid)"; } }

Этот SQL-запрос был получен при запуске psql с параметром -E и выполнении команды \ds:

demo_portal=> \ds ********* QUERY ********** SELECT n.nspname as "Schema", c.relname as "Name", CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' WHEN 'i' THEN 'index' WHEN 'S' THEN 'sequence' WHEN 's' THEN 'special' WHEN 'f' THEN 'foreign table' END as "Type", pg_catalog.pg_get_userbyid(c.relowner) as "Owner" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('S','') AND n.nspname <> 'pg_catalog' AND n.nspname <> 'information_schema' AND n.nspname !~ '^pg_toast' AND pg_catalog.pg_table_is_visible(c.oid) ORDER BY 1,2; **************************

В конфигурации Hibernate указываем диалект:

<property name="hibernate.dialect">by.tychina.PostgreSQL9MultiTenantDialect</property>



Автоматическое обновление схемы

В конфигурации Hibernate опция hbm2ddl.auto закомментирована, поскольку Hibernate не знает, как обновлять схему базы данных при использовании выбранной стратегии мультитенантности.

Для автоматического обновления схемы напишем собственную реализацию (практически полностью скопированную из исходного кода Hibernate):

package by.tychina.schema; import by.tychina.ConnectionProvider; import by.tychina.Persistence; import by.tychina.tenant.TenantId; import org.hibernate.cfg.Configuration; import org.hibernate.cfg.Environment; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.internal.FormatStyle; import org.hibernate.engine.jdbc.internal.Formatter; import org.hibernate.tool.hbm2ddl.DatabaseMetadata; import org.hibernate.tool.hbm2ddl.SchemaUpdateScript; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.List; /** * @author Sergey Tychina */ public class MultiTenantHibernateSchemaUpdate { public static void execute(TenantId tenantId) { Connection connection = ConnectionProvider.getConnection(tenantId); Configuration configuration = Persistence.getInstance().

getConfiguration(); String originalSchema = configuration.getProperty(Environment.DEFAULT_SCHEMA); Statement stmt = null; try { Dialect dialect = Dialect.getDialect(configuration.getProperties()); Formatter formatter = FormatStyle.DDL.getFormatter(); configuration.setProperty(Environment.DEFAULT_SCHEMA, tenantId.getTenantId()); DatabaseMetadata meta = new DatabaseMetadata(connection, dialect, configuration); stmt = connection.createStatement(); List<SchemaUpdateScript> scripts = configuration.generateSchemaUpdateScriptList(dialect, meta); for (SchemaUpdateScript script : scripts) { String formatted = formatter.format(script.getScript()); stmt.executeUpdate(formatted); } } catch (SQLException e) { throw new RuntimeException(e); } finally { if (originalSchema != null) { configuration.setProperty(Environment.DEFAULT_SCHEMA, originalSchema); } else { configuration.getProperties().

remove(Environment.DEFAULT_SCHEMA); } try { if (stmt != null) { stmt.close(); } connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

Порядок работы:

  • Установите текущую схему в конфигурации Hibernate, используя

    configuration.setProperty(Environment.DEFAULT_SCHEMA, tenantId.getTenantId());

  • Сгенерируйте сценарии для обновления схемы, используя

    List<SchemaUpdateScript> scripts = configuration.generateSchemaUpdateScriptList(dialect, meta);

  • Применять сценарии

    for (SchemaUpdateScript script : scripts) { String formatted = formatter.format(script.getScript()); stmt.executeUpdate(formatted); }

  • Восстановить исходное имя схемы в конфигурации

    if (originalSchema != null) { configuration.setProperty(Environment.DEFAULT_SCHEMA, originalSchema); } else { configuration.getProperties().

    remove(Environment.DEFAULT_SCHEMA); }

При запуске приложения после инициализации Hibernate просто вызовите

MultiTenantHibernateSchemaUpdate.execute(tenantId);

и диаграмма будет обновлена.

Спасибо.

Теги: #java #hibernate #envers #postgresql #мультитенантность #java

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.