Проблема совместимости типов данных PostgreSQL и H2 при создании таблиц из Entity

Проблема совместимости типов данных 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

✅ Расширяемость — легко добавить другие типы

(Просмотрено 2 раз, 2 раз за сегодня)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *