Хотите воспользоваться преимуществами (и недостатками) автоматического обновления схемы базы данных при использовании 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); }
MultiTenantHibernateSchemaUpdate.execute(tenantId);
и диаграмма будет обновлена.
Спасибо.
Теги: #java #hibernate #envers #postgresql #мультитенантность #java
-
Шрифт С Точками Вместо Букв
19 Oct, 24 -
Ошибка В Работе L7 В Микротике
19 Oct, 24 -
Gsm Сигнализация На Окна И Двери
19 Oct, 24 -
Каково Преподавать В It-Курсе?
19 Oct, 24