JavaRush /Курсы /Hibernate deep-dive /Стратегии id: IDENTITY

Стратегии id: IDENTITY, SEQUENCE, UUID

Hibernate deep-dive
15 уровень , 1 лекция
Открыта

1. Что сравниваем: стратегии id и SQL

Мы уже разложили id на несколько вопросов, и сейчас сузим фокус до самого болезненного: кто вообще выдаёт значение и в какой момент Hibernate успевает его узнать. Когда смотришь на аннотацию @GeneratedValue, хочется думать: “Ну это же просто как поле заполняется, какая разница?”. Но Hibernate воспринимает идентификатор как часть своей внутренней математики: пока у объекта нет понятной идентичности, его сложнее учитывать в persistence context, сложнее выстраивать порядок INSERT‑ов и сложнее гарантировать корректную синхронизацию графа. Поэтому стратегия id — это не “украшение модели”, а реальный переключатель поведения.

В этой лекции мы сравним четыре практические стратегии именно с точки зрения insert‑потока: кто генерирует значение и когда Hibernate может его узнать. Вам важно удержать две оси, потому что дальше почти все эффекты (ранний INSERT, невозможность batching, дополнительные round‑trip’ы к базе) окажутся просто следствием этих двух вопросов.

Стратегия Кто генерирует id Когда id становится известен Hibernate
IDENTITY база данных только во время INSERT
SEQUENCE база данных (sequence) до INSERT (Hibernate может заранее взять nextval)
UUID Java (приложение / Hibernate) до INSERT (без обращения к БД)
assigned id ваш код ещё до persist() (если вы не забыли)

Чтобы не быть голословными, ниже мы будем смотреть на одну и ту же идею через три линзы: что происходит в памяти, как выглядит SQL‑трейс и почему это влияет на “чувство контроля” над вставками.

2. IDENTITY: id из базы → INSERT сразу

IDENTITY часто выглядит как самый “естественный” вариант: колонка автоинкрементится, база сама выдаёт число, и жизнь прекрасна. Но у Hibernate есть маленькая проблема: чтобы поставить id в Java‑объект, ему нужно выполнить реальный INSERT и получить сгенерированное значение. А значит, он теряет главное удовольствие ORM‑подхода — возможность спокойно складывать INSERT‑ы в очередь и отправлять их одной пачкой на flush().

На практике это означает, что persist() для IDENTITY очень часто “внезапно” приводит к SQL раньше, чем вы ожидаете. И это не каприз ORM, а логическое следствие: Hibernate не может сказать объекту “твой id = …”, пока база не сказала это Hibernate. Представьте очередь в МФЦ: пока вам не выдали талончик с номером, вы не можете “официально” участвовать в очереди. С IDENTITY талончик выдаётся только на входе, и каждый человек обязан подойти к стойке отдельно.

Мини‑пример entity с IDENTITY

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
class LabIdentityProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // id появится только после реального INSERT в БД
    private Long id;

    private String name; // обычное поле: само по себе не влияет на момент генерации id
}

Как это выглядит в SQL (упрощённо)

Для PostgreSQL типичный паттерн — insert ... returning id (или получение generated keys через JDBC), и вы увидите примерно такую идею:

insert into lab_identity_product (name) values (?) returning id;

Важно не конкретное “returning”, а факт: вставка должна случиться, чтобы id стал известен.

Почему IDENTITY плохо сочетается с batching

Мы не уходим в настройки batching (это будет отдельная большая история), но смысл можно понять прямо сейчас. Batch insert — это когда Hibernate пытается отправить несколько INSERT как “пакет” одинаковых statement’ов. При IDENTITY после каждого INSERT Hibernate обязан получить конкретный id и записать его обратно в конкретный объект. В результате вставки становятся последовательными и “синхронными”: вставили — получили id — вставили — получили id. Даже если вы включили batching флагом, IDENTITY в большинстве случаев сделает так, что insert batching для сущности не заработает, потому что механика получения id это ломает.

Нюанс, который часто удивляет

Если вы привыкли думать: “SQL уйдёт на flush/commit”, то с IDENTITY вы будете регулярно ловить ощущение “Hibernate предал меня”. На самом деле он просто честно выполняет контракт генерации id. Поэтому, когда вы анализируете SQL‑лог и видите INSERT раньше, чем ожидали, один из первых вопросов должен быть: “А какая у нас стратегия id?”.

3. SEQUENCE: id заранее, INSERT позже

После IDENTITY стратегия SEQUENCE обычно ощущается как “ORM‑friendly”. Причина простая: sequence — это отдельный объект БД, который умеет выдавать новые значения (nextval) без вставки строки. Hibernate может взять id заранее, назначить его сущности в памяти, пометить сущность как managed, и уже потом, на flush(), отправить INSERT. Получается более управляемый поток: id есть, а строки в таблице ещё может не быть.

Если провести аналогию, то SEQUENCE — это как взять номерок в электронной очереди в приложении ещё дома. Номер уже у вас, вы уже “участник процесса”, но к окну вы подойдёте позже, когда придёте в офис. Hibernate от этого счастлив: он может собирать пачку вставок и отправлять их более эффективно.

Мини‑пример entity с SEQUENCE

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;

@Entity
class LabSequenceProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "lab_seq_product_seq") // id можно получить до INSERT
    @SequenceGenerator(
            name = "lab_seq_product_seq",
            sequenceName = "lab_seq_product_seq" // имя sequence в БД (её нужно создать миграцией)
    )
    private Long id;

    private String name;
}

Обратите внимание: в реальном проекте с Flyway вы обязаны создать sequence миграцией. Hibernate не должен “додумывать схему” за вас.

Фрагмент миграции Flyway (идея)

create sequence lab_seq_product_seq start with 1 increment by 1;

create table lab_sequence_product (
    id   bigint not null primary key,
    name text   not null
);

Здесь важная мысль: колонка id не обязана иметь default nextval(...). Hibernate сам может вызывать nextval перед INSERT (и обычно так и делает). Наличие default — отдельный выбор, но для понимания insert‑потока это не принципиально.

Как это выглядит в SQL‑трейсе

Часто вы увидите два шага: сначала получение значения из sequence, потом вставку (вставка может быть позже, на flush()):

select nextval('lab_seq_product_seq');
insert into lab_sequence_product (name, id) values (?, ?);

Да, это дополнительный запрос к БД — но он позволяет Hibernate не привязывать id к факту вставки.

Почему SEQUENCE дружит с batching

Поскольку id известен заранее, Hibernate может сформировать пачку INSERT‑ов одинаковой формы и отправить их как batch, а не как серию “вставил — получил id — вставил — получил id”. У него нет необходимости “останавливаться” после каждой строки ради id.

Есть ещё один тонкий плюс: когда вы создаёте граф сущностей и вам нужно проставлять ссылки (например, “ребёнок ссылается на родителя”), заранее известный id иногда упрощает жизнь. Не путайте это с тем, что Hibernate “везде использует id для связей” — это скорее про внутреннюю организацию и предсказуемость.

Про “пропуски” в номерах

Интуитивно хочется: “Sequence — значит 1,2,3,4 без пропусков”. На практике пропуски — нормальны и неизбежны. Транзакция может откатиться, приложение может упасть, Hibernate может брать диапазоны значений “про запас” (оптимизации). Поэтому последовательность — это генератор уникальных чисел, а не устройство для красивых “идеально сплошных” порядковых номеров для бухгалтерии.

4. UUID: id без базы, цена в индексах

После числовых id UUID часто воспринимается как “современный и удобный” вариант: можно генерировать идентификаторы без обращения к базе, удобно для распределённых систем, можно иметь id до вставки строки, и вообще “всё выглядит взрослым”. В реальности это действительно удобный инструмент — но как и любой инструмент, он имеет цену. Причём цена в основном не в Hibernate, а в физике хранения и индексации в базе.

С точки зрения insert‑потока UUID ведёт себя очень похоже на SEQUENCE в одном ключевом моменте: id известен до INSERT. Разница в том, что UUID генерируется на Java‑стороне, то есть лишнего round‑trip’а к базе за nextval нет. Но зато вставляемое значение больше, менее читабельно в логах и обычно хуже для locality индекса (если UUID случайный).

Мини‑пример entity с UUID

import java.util.UUID;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
class LabUuidProduct {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID) // id генерируется на стороне приложения, без обращения к БД
    private UUID id;

    private String name;
}

Как выглядит вставка

SQL будет выглядеть как обычный INSERT, просто с UUID‑параметром:

insert into lab_uuid_product (name, id) values (?, ?);

При этом “фокус” происходит ещё до SQL: Hibernate уже назначил id объекту в памяти. Это легко увидеть даже без базы: вы можете напечатать id после persist() и он уже будет.

Почему UUID может быть дороже, чем кажется (на уровне базы)

UUID обычно занимает больше места, чем bigint. Индексы по UUID тяжелее, сравнение UUID тяжелее, а если UUID случайный, вставки будут “раскиданы” по B‑Tree индексу и могут создавать больше page splits. PostgreSQL с типом uuid работает адекватно, но “адекватно” не означает “бесплатно”.

В лабораторном проекте это особенно чувствуется на сущностях, которые участвуют в массовых вставках или очень частых join’ах. Впрочем, не надо делать из этого религию: UUID иногда реально оправдан, особенно если вам нужно формировать id на стороне приложения до вставки, или если сущность живёт на границе интеграции и id приходит не из вашей БД.

5. Assigned id: id задаёт ваш код

Assigned id — это ситуация, когда вы не ставите @GeneratedValue и сами задаёте значение @Id. Для Hibernate это максимально “прозрачная” стратегия: никакой магии, никакой генерации, никакого “а где моя sequence”. Но за прозрачность вы платите ответственностью: вы обязаны гарантировать уникальность, обязаны установить id до persist(), и обязаны понимать, откуда он вообще берётся.

С точки зрения insert‑потока assigned id тоже “удобен”: id известен заранее, Hibernate может складывать INSERT‑ы в очередь, batching возможен (по крайней мере концептуально). Проблема обычно не в производительности, а в человеческом факторе: забыли установить id — получите исключение; установили дубль — получите constraint violation; поменяли источник id — сломали совместимость.

Мини‑пример entity с assigned id

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
class ExternalCatalogItem {

    @Id
    private String externalId; // ключ приходит извне: вы обязаны заполнить его сами до persist()

    private String name;
}

Где чаще всего используют assigned id

Самый здоровый сценарий — когда id реально приходит извне и вы не можете или не хотите подменять его внутренним числом. Например, импорт из внешнего каталога, интеграция с партнёром, данные из legacy‑системы. В таком мире внешний id — не “просто поле”, а часть контракта: он должен быть стабилен.

Как выглядит типичная проблема в коде

ExternalCatalogItem item = new ExternalCatalogItem();
// item.setExternalId("EXT-123"); // забыли
entityManager.persist(item);      // бах — и Hibernate не знает, что вставлять в PK

Hibernate не сможет вставить строку, потому что primary key — это не “optional” штука. В зависимости от настроек вы получите исключение либо на persist(), либо на flush(), но смысл один: assigned id требует дисциплины.

6. Сравнение по insert‑потоку

После деталей легко утонуть в нюансах, поэтому полезно собрать сравнение в одну таблицу. Это не “рейтинг лучших стратегий”, а шпаргалка: что вы платите и что получаете именно в вставках.

Критерий IDENTITY SEQUENCE UUID assigned id
id появляется в объекте после реального INSERT до INSERT до INSERT до persist()
вставка может отложиться до flush обычно нет (часто “раньше”) да да да
дополнительный round‑trip нет (но есть необходимость вставить) да (nextval) нет нет
insert batching (концептуально) почти всегда плохо обычно хорошо обычно хорошо обычно хорошо
простота глазами новичка кажется простым “чуть сложнее” “моднее” “сам сделаю”
типовые риски ранние INSERT, хуже batch‑поток пропуски в значениях, нужен DDL большие индексы, тяжёлые ключи дисциплина, уникальность, дубли

Ещё полезна схема, чтобы буквально увидеть момент, где IDENTITY “ломает” отложенную вставку:

flowchart TD
    A["persist(entity)"] --> B{"Стратегия id?"}

    B -->|IDENTITY| C["Hibernate вынужден сделать INSERT"]
    C --> D["БД генерирует id"]
    D --> E["id присвоен объекту"]

    B -->|SEQUENCE| F["Hibernate делает nextval()"]
    F --> G["id присвоен объекту"]
    G --> H["INSERT откладывается до flush/commit"]

    B -->|UUID| I["Hibernate генерирует UUID в Java"]
    I --> J["id присвоен объекту"]
    J --> K["INSERT откладывается до flush/commit"]

    B -->|assigned| L["Ваш код уже присвоил id"]
    L --> M["Hibernate принимает id"]
    M --> N["INSERT откладывается до flush/commit"]

Если вы запомните только эту диаграмму — уже будет неплохо. Остальные нюансы — это вариации вокруг неё.

Разница в SQL‑логе Commerce Persistence Lab

Очень хочется превращать обсуждение стратегий в философию и “войны подходов”. Но мы на deep‑dive курсе, поэтому критерий простой: смотрите SQL‑лог и задавайте себе вопрос “почему он ушёл именно сейчас”.

Представьте, что вы в lab‑коде делаете примерно такой сценарий: создаёте три сущности в одной транзакции, но не вызываете flush() вручную и не делаете запросов.

import jakarta.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

class InsertFlowScenario {

    private final EntityManager em;

    InsertFlowScenario(EntityManager em) {
        this.em = em;
    }

    @Transactional
    void run() {
        // Важно: здесь мы только регистрируем сущности в persistence context.
        // Когда именно появится SQL — зависит от стратегии генерации id.
        em.persist(new LabSequenceProduct("A"));
        em.persist(new LabSequenceProduct("B"));
        em.persist(new LabSequenceProduct("C"));

        // commit завершит транзакцию и вызовет flush
        // (и вот там Hibernate обычно отправит INSERT'ы, если ему не пришлось делать это раньше)
    }
}

Если это SEQUENCE, то вы часто увидите: сначала несколько select nextval(...), а insert — ближе к концу транзакции (на flush()). Если это IDENTITY, то вы можете увидеть insert сразу после каждого persist(). И это как раз тот момент, когда у разработчика “ломается интуиция”, если он думал, что persist всегда “без SQL”.

Отдельно полезно смотреть на логи не только глазами “сколько запросов”, но и глазами “какая форма запросов”. IDENTITY часто выдаёт форму “insert returning”, а SEQUENCE — отдельные “select nextval” плюс обычный insert.

7. Типичные ошибки при выборе стратегии id

Ошибка №1: выбирать IDENTITY “потому что так проще” и удивляться ранним INSERT.
Новички часто ставят IDENTITY на любой Long id, потому что это выглядит как минимальный набор аннотаций. Потом они включают SQL‑trace и видят, что вставки уходят раньше flush(), и начинают искать “кто вызывает flush”. Проблема не в flush. Проблема в том, что для IDENTITY вставка — это способ получить id. Если вы хотите более управляемый insert‑поток, нужно осознанно выбирать стратегию, в которой id доступен заранее.

Ошибка №2: ожидать от SEQUENCE красивой нумерации без пропусков.
Секвенс выдаёт уникальные значения, а не “идеальный порядковый номер”. Откаты транзакций, перезапуски приложения и оптимизации выдачи диапазонов значений неизбежно создают gaps. Если вам нужен красивый “номер заказа без пропусков”, это вообще другая бизнес‑задача и решается по‑другому, а не через надежду на sequence.

Ошибка №3: брать UUID как “просто моднее”, не думая о цене хранения и индексов.
UUID удобен тем, что появляется без базы и до вставки. Но это не бесплатный бонус: вы храните более тяжёлые ключи, строите более тяжёлые индексы, и чтения/джойны могут стать дороже. UUID — хороший инструмент, когда он решает реальную инженерную задачу (например, id нужен до INSERT и вы не хотите/не можете обращаться к sequence). Если задача этого не требует — UUID может стать красивым, но дорогим аксессуаром.

Ошибка №4: использовать assigned id и забывать присвоить значение до persist().
Assigned id — это “максимум контроля”, но контроль требует дисциплины. Если id не установлен, Hibernate не сможет вставить строку (PK не может быть null). И это обычно всплывает не сразу: вы написали код, он “почти работает”, а потом в одном сценарии забыли заполнить поле — и всё упало на flush(). С assigned id полезно воспринимать установку идентификатора как часть конструктора/фабрики, а не как “сеттер где‑нибудь потом”.

Ошибка №5: пытаться выводить “человеческий смысл” из технического id.
Когда разработчик видит последовательный Long id, ему хочется использовать его как “номер сущности” и показывать пользователю или завязывать на него бизнес‑логику. Но технический id — это прежде всего механизм идентичности строки, а не бизнес‑атрибут. Как только вы начинаете относиться к нему как к бизнес‑смыслу, вы резко ограничиваете себе свободу менять стратегию, оптимизировать вставки и эволюционировать схему.

1
Задача
Hibernate deep-dive, 15 уровень, 1 лекция
Недоступна
Сравнение IDENTITY и SEQUENCE по SQL-потоку
Сравнение IDENTITY и SEQUENCE по SQL-потоку
1
Задача
Hibernate deep-dive, 15 уровень, 1 лекция
Недоступна
UUID и assigned id до INSERT
UUID и assigned id до INSERT
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ