Проблема совместимости типов данных PostgreSQL и H2 при создании таблиц из Entity
Описание проблемы
При разработке приложения на Quarkus с использованием Hibernate и PostgreSQL в production-окружении и H2 для тестов возникает проблема несовместимости типов данных. Hibernate автоматически генерирует DDL-скрипты на основе Entity-классов, но некоторые типы данных PostgreSQL не поддерживаются H2.
Примеры типичных ошибок:
1. Тип INTERVAL
Unknown data type: "INTERVAL"
Error executing DDL "create table vsm.backup_scheduled_tasks (
task_delay interval second(18,9) not null, ...)"
2. Тип TINYINT
Unknown data type: "TINYINT"
Error executing DDL "create table idp.AUTHENTICATION_EXECUTION (
REQUIREMENT tinyint check (REQUIREMENT between 0 and 3), ...)"
Корневая причина
- Hibernate использует диалект БД для генерации DDL
- PostgreSQL поддерживает типы INTERVAL, которые H2 не распознаёт
- H2 не имеет типа TINYINT (есть только SMALLINT, INTEGER, BIGINT)
- Автоматическая маппинг типов:
- java.time.Duration → PostgreSQL INTERVAL → ❌ H2 не понимает
- byte/Byte → TINYINT → ❌ H2 не поддерживает
Решение: Кастомный диалект H2
Создаём класс H2CustomDialect, который перехватывает DDL-скрипты перед их выполнением и заменяет несовместимые типы:
package ru.basistech.virtualsecurity.admin.core.jpa;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.tool.schema.spi.Exporter;
/**
* Кастомный диалект для H2, который заменяет несовместимые типы данных PostgreSQL.
* <p>
* <strong>Проблема:</strong> Hibernate генерирует DDL-скрипты на основе Entity-классов,
* используя типы данных целевой БД (PostgreSQL в production). При запуске тестов на H2
* возникают ошибки из-за несовместимости типов.
* <p>
* <strong>Решение:</strong> Перехватываем DDL-скрипты перед выполнением и заменяем:
* <ul>
* <li>{@code INTERVAL} (любой вариант) → {@code BIGINT}</li>
* <li>{@code INTERVAL SECOND(p,s)} → {@code BIGINT}</li>
* <li>{@code TINYINT} → {@code SMALLINT}</li>
* </ul>
* <p>
* <strong>Автоматическая конвертация Hibernate:</strong>
* <ul>
* <li>{@link java.time.Duration} → {@code BIGINT} (наносекунды)</li>
* <li>{@code byte/Byte} → {@code SMALLINT} (H2 не имеет TINYINT)</li>
* </ul>
* <p>
* <strong>Важно:</strong> Замена происходит только на уровне SQL-строк.
* Entity-классы и маппинг полей не изменяются.
*/
public class H2CustomDialect extends H2Dialect {
/**
* Переопределяем экспортёр таблиц для замены несовместимых типов данных.
* <p>
* <strong>Механизм работы:</strong> Стандартный {@code StandardTableExporter}
* генерирует SQL-скрипты создания таблиц. Мы оборачиваем его в прокси,
* который перед выполнением заменяет в SQL-строках проблемные типы.
* <p>
* <strong>Применяемые regex-паттерны:</strong>
* <ol>
* <li>{@code (?i)\binterval(\s+second\s*\(\d+\s*,\s*\d+\))?} — заменяет
* как {@code INTERVAL}, так и {@code INTERVAL SECOND(18,9)} на {@code BIGINT}</li>
* <li>{@code (?i)\btinyint\b} — заменяет {@code TINYINT} на {@code SMALLINT}</li>
* </ol>
* <p>
* <strong>Примеры преобразований:</strong>
* <ul>
* <li>{@code task_delay interval not null}
* → {@code task_delay bigint not null}</li>
* <li>{@code task_delay interval second(18,9) not null}
* → {@code task_delay bigint not null}</li>
* <li>{@code REQUIREMENT tinyint check (REQUIREMENT between 0 and 3)}
* → {@code REQUIREMENT smallint check (REQUIREMENT between 0 and 3)}</li>
* </ul>
*
* @param <T> тип экспортируемого объекта (обычно {@link org.hibernate.boot.model.relational.Exportable})
* @return экспортёр с автоматической заменой типов данных
*/
@Override
public <T> Exporter<T> getTableExporter() {
Exporter<T> exporter = super.getTableExporter();
return (target, exportOptions) -> {
String[] originalSql = exporter.getSqlCreateStrings(target, exportOptions);
for (int i = 0; i < originalSql.length; i++) {
// Заменяем INTERVAL и INTERVAL SECOND(p,s) на BIGINT одним regex
originalSql[i] = originalSql[i].replaceAll(
"(?i)\\binterval(\\s+second\\s*\\(\\d+\\s*,\\s*\\d+\\))?",
"bigint"
);
// Заменяем TINYINT на SMALLINT (H2 не поддерживает TINYINT)
originalSql[i] = originalSql[i].replaceAll("(?i)\\btinyint\\b", "smallint");
}
return originalSql;
};
}
}
Настройка проекта
1. Подключение диалекта в application.properties:
# Для тестов (профиль test)
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
%test.quarkus.hibernate-orm.dialect=ru.basistech.virtualsecurity.admin.core.jpa.H2CustomDialect
%test.quarkus.hibernate-orm.database.generation=drop-and-create
2. Maven-зависимость для H2:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
<scope>test</scope>
</dependency>
Как это работает?
1. Hibernate генерирует DDL на основе Entity:
@Entity
public class BackupScheduledTaskEntity {
@Column(nullable = false)
private Duration taskDelay; // → INTERVAL SECOND(18,9) в PostgreSQL
}
2. H2CustomDialect перехватывает SQL:
-- До:
task_delay interval second(18,9) not null
-- После regex-замены:
task_delay bigint not null
3. H2 успешно выполняет скрипт:
CREATE TABLE vsm.backup_scheduled_tasks (
task_delay BIGINT NOT NULL,
...
);
4. Hibernate автоматически конвертирует:
- При сохранении: Duration → Long (наносекунды) → BIGINT
- При чтении: BIGINT → Long → Duration
Преимущества подхода
✅ Без изменения Entity — аннотации остаются прежними
✅ Прозрачность — разработчики не знают о замене типов
✅ Production-безопасность — затрагивает только тесты
✅ Универсальность — работает для любых Entity с Duration и byte
✅ Расширяемость — легко добавить другие типы