Назад к блогу
Java 13 мин чтения

Сужение типов в Hibernate 6: от Integer к Byte, Short и точным JDBC-маппингам

Почему после апгрейда на Hibernate 6 schema validation ругается на TINYINT/SMALLINT, как подобрать Java-тип и когда нужен @JdbcTypeCode.

HibernateJPASpring BootSQL
Сужение типов в Hibernate 6: от Integer к Byte, Short и точным JDBC-маппингам

Что изменилось в Hibernate 6

В Hibernate 5 многие команды спокойно мапили колонку TINYINT на Integer/int, а SMALLINT — тоже на Integer. Драйвер «проглатывал» расширение типа, schema validation часто была мягче, и всё работало годами.

Hibernate 6 перешёл на строгую пару Java type ↔ JDBC type через JavaType / JdbcType / SqlTypes. Рекомендуемый JDBC-тип для Java-типа стал каноническим: byte/Byte → TINYINT, short/Short → SMALLINT, int/Integer → INTEGER. Если в БД уже лежит более узкий тип, а в entity — более широкий, при ddl-auto=validate получаете классическую ошибку wrong column type.

Пример 1: TINYINT больше не «почти Integer»

Типичный legacy-код:

// Hibernate 5 — часто проходило
@Column(name = "status_code")
private Integer statusCode; // колонка TINYINT

После апгрейда на Spring Boot 3 / Hibernate 6:

Schema-validation: wrong column type encountered;
found [tinyint (Types#TINYINT)], but expecting [integer (Types#INTEGER)]

Правильное сужение Java-типа под DDL:

@Column(name = "status_code")
private Byte statusCode; // или byte, если null не нужен

Если менять API домена нельзя (публичный DTO ждёт Integer), оставьте поле как есть, но явно скажите Hibernate JDBC-тип:

@JdbcTypeCode(SqlTypes.TINYINT)
@Column(name = "status_code")
private Integer statusCode;

Либо точечно:

@JdbcType(TinyIntJdbcType.class)
private Integer statusCode;

Пример 2: SMALLINT → Short

Та же история для двухбайтовых колонок:

// Было
private Integer countryId; // SMALLINT в PostgreSQL/MySQL

// Стало логичнее
private Short countryId;

// Или явный override
@JdbcTypeCode(SqlTypes.SMALLINT)
private Integer countryId;

Сужение типов — не «придирка фреймворка», а защита от молчаливой потери данных и от рассинхрона DDL ↔ entity при миграциях Flyway/Liquibase.

Пример 3: enum и смена дефолта

В Hibernate 6.1+ ordinal-enum по умолчанию ушёл с TINYINT на SMALLINT (у enum теоретически больше 255 значений). На MySQL/MariaDB/H2 это часто всплывает при validate:

public enum OrderStatus { NEW, PAID, SHIPPED, CANCELLED }

@Enumerated(EnumType.ORDINAL)
private OrderStatus status;

Варианты миграции:

  • расширить колонку до SMALLINT миграцией;
  • зафиксировать старый JDBC-тип: @JdbcTypeCode(SqlTypes.TINYINT);
  • лучше для эволюции схемы — @Enumerated(EnumType.STRING) + VARCHAR/ENUM БД.
@Enumerated(EnumType.ORDINAL)
@JdbcTypeCode(SqlTypes.TINYINT)
private OrderStatus status;

// Предпочтительнее для читаемости и миграций
@Enumerated(EnumType.STRING)
@Column(length = 32)
private OrderStatus status;

Пример 4: boolean — BOOLEAN, BIT или TINYINT

На PostgreSQL Boolean спокойно мапится на BOOLEAN. На MySQL historically часто BIT(1) или TINYINT(1). После апгрейда диалект может ожидать другой код типа, чем в вашей старой схеме.

// Явно под MySQL tinyint(1)
@JdbcTypeCode(SqlTypes.TINYINT)
private Boolean active;

// Или под BIT
@JdbcTypeCode(SqlTypes.BIT)
private Boolean active;

Правило то же: либо подгоняете Java/JDBC-аннотацию под существующую колонку, либо меняете DDL миграцией — не оба сразу «наугад».

Пример 5: UUID, время и «старые» Timestamp

Hibernate 6 лучше дружит с java.time и нативными UUID:

@JdbcTypeCode(SqlTypes.UUID)
private UUID id;

private Instant createdAt;          // timestamptz / timestamp
private OffsetDateTime updatedAt;   // предпочтительнее java.util.Date

Мапинг java.util.Date / Calendar всё ещё жив, но на новых проектах его сужают до Instant/LocalDateTime/OffsetDateTime, чтобы убрать двусмысленность таймзоны. Для PostgreSQL uuid колонки не тащите строковый VARCHAR(36) без необходимости — это лишние преобразования и индексы другого размера.

Пример 6: AttributeConverter вместо «магического» Integer

Когда в БД хранится код TINYINT, а в домене нужен богатый тип:

@Converter(autoApply = true)
public class PriorityConverter implements AttributeConverter<Priority, Byte> {
  public Byte convertToDatabaseColumn(Priority p) {
    return p == null ? null : p.getCode(); // 1..5
  }
  public Priority convertToEntityAttribute(Byte code) {
    return Priority.fromCode(code);
  }
}

private Priority priority; // колонка TINYINT, Java — enum/value object

Так вы сужаете JDBC-тип до реальной колонки и одновременно поднимаете уровень доменной модели.

Как диагностировать быстро

  1. Включите spring.jpa.hibernate.ddl-auto=validate на staging — ошибки всплывут до прода.
  2. Сверьте DDL (Flyway) с entity: TINYINT/SMALLINT/INT/BIGINT.
  3. Смотрите migration guide Hibernate 6.0 → 6.2 → 6.4: enum, UUID, JSON, array types.
  4. Не лечите всё через columnDefinition — это ломает переносимость диалектов. Предпочтительнее @JdbcTypeCode / смена Java-типа / миграция колонки.

Практический чеклист миграции

  • TINYINT → Byte/byte (или @JdbcTypeCode(SqlTypes.TINYINT) на Integer).
  • SMALLINT → Short/short.
  • Ordinal enum → проверить SMALLINT vs старый TINYINT.
  • Boolean → сверить с BIT/BOOLEAN/TINYINT конкретной СУБД.
  • Время → Instant/OffsetDateTime; UUID → UUID + SqlTypes.UUID.

Вывод

«Сужение типов» в Hibernate 6 — это про честный контракт между колонкой и полем entity. Раньше int часто маскировал TINYINT; сейчас фреймворк требует либо точный Java-тип (byte/short), либо явный @JdbcTypeCode, либо миграцию DDL. Закладывайте это в апгрейд Spring Boot 2→3: один день на аудит колонок экономит неделю странных validate-ошибок на каждом окружении.

Алексей Прыбытков

Senior Software Developer

© 2026 Все права защищены