1. Роль @Id в entity
Если @Entity — это «паспорт гражданина JPA», то @Id — это номер паспорта, без которого гражданин как бы есть, но в системе его нет. Hibernate как ORM не просто хранит объект и потом «как-нибудь» решает, что с ним делать. Ему нужно однозначно отвечать на вопрос: «Этот объект — новая запись или уже существующая?»
На SQL-уровне вы уже знаете ответ: в таблице запись идентифицируется первичным ключом (PRIMARY KEY). В JPA это отражается полем, помеченным @Id.
Минимальная форма — специально максимально простая:
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity // Говорим JPA/Hibernate: этот класс нужно маппить на таблицу
public class Category {
@Id // Это первичный ключ сущности (то, по чему Hibernate отличает записи)
private Long id; // Long, чтобы до сохранения можно было иметь null (id ещё не назначен)
}
Здесь важны две практические мысли, которые начинающие часто пропускают:
Первая: @Id — это не «какое-нибудь поле», а поле, которое связывает объектный мир с реляционной идентичностью. ORM не может нормально работать с сущностью без идентификатора: тогда она не понимает, что обновлять, что удалять и как отличать один объект от другого.
Вторая: почти всегда мы начинаем с типа Long, а не с long. Причина простая и очень житейская: до сохранения в базу id у новой сущности обычно ещё нет, и в Java это естественно выражается через null. Если взять long, то «пустым значением» станет 0, и вы сами устроите себе мини-цирк: сущность вроде новая, а id уже 0, и вы сидите с вопросом «что вообще происходит?». Поэтому Long — не каприз, а нормальная инженерная осторожность.
2. Ручное присваивание id: просто выглядит, сложно жить
Иногда хочется сделать «по-простому»: раз у поля id тип Long, значит я сам буду задавать значения — и никаких @GeneratedValue. Особенно соблазнительно это после знакомства с обычными объектами Java: «ну я же могу написать setId(1L), почему бы и нет?». Можете. Но тогда вы автоматически соглашаетесь на роль «мини-базы данных».
Пример ручного id выглядит так (обратите внимание: нет @GeneratedValue):
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity // Сущность есть...
public class Category {
@Id // ...но id мы обязуемся назначать сами
private Long id;
}
И где-то в коде вы делаете что-то вроде (просто иллюстрация идеи):
Category category = new Category();
category.setId(100L); // Вы сами придумали id и берёте на себя ответственность за уникальность
Почему в прикладном сервисе это обычно плохая идея:
Вы отвечаете за уникальность. В таблице PRIMARY KEY всё равно потребует уникальности. Если вы случайно поставите уже существующий id, получите конфликт. Если вы не случайно, а «параллельно в двух потоках» придумали одно и то же значение, конфликт будет тем более. А если однажды захотите масштабировать приложение, даже просто запустив два экземпляра, — такой конфликт начнёт приходить уже «по расписанию».
Есть ситуации, где ручной id оправдан. Например, вы импортируете справочник из внешней системы, и внешний id действительно является устойчивым идентификатором. Но в нашем учебном mini-shop мы не строим систему вокруг внешних идентификаторов, поэтому ручное присваивание — скорее демонстрация того, что так можно, чем стиль, который стоит выбирать «по умолчанию».
3. @GeneratedValue и генерация id
В реальной жизни почти всегда хочется, чтобы технический идентификатор создавался автоматически и этим занималась база данных — потому что именно для таких задач она и предназначена. В JPA для этого существует @GeneratedValue. И здесь полезно сразу убрать одну иллюзию: @GeneratedValue — не «магия Hibernate», а просто декларация: источник значения id — не я, а стратегия генерации.
Минимальный кусочек кода выглядит так:
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Id // Поле является первичным ключом сущности
@GeneratedValue(strategy = GenerationType.IDENTITY) // Значение будет генерировать база при INSERT
private Long id; // До вставки в БД тут обычно null
Стратегий несколько, но сегодня нас интересуют две, которые действительно важны в мире PostgreSQL и которые вы обязаны различать уже сейчас:
GenerationType.IDENTITY — база генерирует id при вставке строки в таблицу.
GenerationType.SEQUENCE — база выдаёт id из отдельного объекта-генератора, который называется sequence (последовательность).
Можно встретить GenerationType.AUTO, который означает «пусть провайдер сам выберет». Но в учебном проекте это часто плохая идея: вам важно понимать, почему SQL выглядит именно так, а не гадать, «какую стратегию он там выбрал сегодня». Поэтому мы будем стараться писать стратегию явно.
Чтобы не держать всё в голове, полезно зафиксировать сравнение в виде небольшой таблицы.
| Критерий | IDENTITY | SEQUENCE |
|---|---|---|
| Где живёт генератор | В колонке таблицы (identity column) | Отдельный объект БД: sequence |
| Когда становится известен id | После INSERT (БД вернула значение) | До INSERT (мы взяли nextval) |
| Типичный SQL-рисунок | insert ... returning id | select nextval(...) → insert ... (id, ...) |
| PostgreSQL-стиль | Нормально, но “чуть более свежий” синтаксис | Классика PostgreSQL, очень распространено |
| Частая «боль новичка» | «Почему id появляется только после вставки?» | «Почему id прыгают/пропускаются?» |
Дальше разберём обе стратегии отдельно и очень приземлённо — так, чтобы вы потом могли открыть SQL-лог и понимать, что именно там происходит.
4. GenerationType.IDENTITY: «сначала INSERT — потом узнаем id»
IDENTITY — это подход «пусть таблица сама выдаёт новый номер строки». По-человечески это похоже на кофейню: вы делаете заказ, и только после этого касса печатает чек с номером. Пока чек не напечатан, номер вам неизвестен — он возникает в момент операции.
В JPA это выглядит так:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity // Маппим класс на таблицу
public class Category {
@Id // PK в терминах JPA
@GeneratedValue(strategy = GenerationType.IDENTITY) // id будет известен только после INSERT
private Long id;
}
Что обычно происходит в PostgreSQL на уровне DDL: колонка объявляется как identity. Упрощённо это может выглядеть так:
create table category (
id bigint generated by default as identity primary key
);
(Синтаксис может различаться в деталях, но идея одна: id генерируется базой автоматически.)
Самый важный практический эффект IDENTITY — id становится известен, когда база выполнила INSERT. То есть Hibernate не может знать его заранее, потому что это значение рождается внутри БД. Поэтому при сохранении новой сущности он вынужден сначала выполнить вставку, чтобы получить ключ.
Если вы включите SQL-логирование, то часто увидите что-то очень похожее по смыслу:
insert into category (code, name) values (?, ?) returning id
-- БД возвращает id, Hibernate кладёт его в поле объекта (после выполнения INSERT)
И здесь возникает популярный вопрос новичка: «Так когда мне ждать, что id появится в объекте?» В рамках этой лекции держим простую ментальную модель: после того, как запись реально вставлена в базу. Не пытайтесь «угадать» id заранее и не воспринимайте его как красивый номер по порядку — это вообще отдельная ловушка, к ней мы ещё вернёмся ниже.
У IDENTITY есть и плюсы: не нужно думать об объектах sequence, модель выглядит проще. Но есть и минусы, о которых полезно знать, даже если вы пока не занимаетесь оптимизацией. Hibernate с IDENTITY часто меньше «маневрирует» при вставках, потому что ему нужно сразу выполнить INSERT, чтобы получить id. В маленьком учебном проекте это не катастрофа, но понимание механики пригодится уже очень скоро, когда вы начнёте смотреть на SQL глазами инженера, а не как на «шум в логах».
5. GenerationType.SEQUENCE: «дайте номерок заранее»
Если IDENTITY — это «касса печатает номер после заказа», то SEQUENCE — это автомат с талончиками в электронной очереди: вы сначала берёте номерок, а потом идёте выполнять действия. В PostgreSQL sequences — родная часть экосистемы: это отдельные объекты БД, которые умеют выдавать уникальные числа командой nextval(...).
В JPA это обычно делается так:
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
@Entity // Маппим на таблицу (обычно category)
public class Category {
@Id // PK в JPA
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // Берём id из sequence
@SequenceGenerator(name = "category_seq", sequenceName = "category_seq") // name — для JPA, sequenceName — имя в БД
private Long id;
}
Тут впервые появляется @SequenceGenerator, и пугаться не нужно: никакой мистики. Мы просто говорим: «генератор в JPA называется category_seq, а в базе есть sequence с именем category_seq».
В PostgreSQL sequence обычно создаётся так (условный пример для понимания):
create sequence category_seq start with 1 increment by 1;
Что вы увидите в SQL-логах при сохранении новой сущности (опять же, по смыслу, без обещаний, что у вас будет слово в слово то же самое):
select nextval('category_seq'); -- получили новое число, например 1 (это происходит ДО insert)
insert into category (id, code, name) values (?, ?, ?); -- вставили строку с этим id
И вот здесь появляется важное отличие от IDENTITY: при SEQUENCE Hibernate может получить id до вставки строки. То есть в памяти у объекта id может стать ненулевым (не null) ещё до того, как вы увидите INSERT, хотя в конечном счёте данные всё равно окажутся в базе только после реальной записи.
Есть ещё одна вещь, которая почти гарантированно удивит новичка: sequences не гарантируют «бездырочный» порядок. Даже если sequence выдаёт числа 1, 2, 3… — это не означает, что id в таблице пойдут идеально подряд, без пропусков. Пропуски появляются по совершенно нормальным причинам: транзакция откатилась, запись удалили, приложение зарезервировало id и не использовало его. В этом нет трагедии, если вы относитесь к id как к техническому идентификатору, а не как к красивому номеру для клиента.
6. @SequenceGenerator: имена и allocationSize
@SequenceGenerator — место, где люди чаще всего делают ошибку в стиле «почти всё правильно, но не работает». Проблема в том, что там одновременно живут два «имени», и новичок легко путает, какое из них про Java, а какое — про базу. Плюс есть настройка allocationSize, которая напрямую влияет на то, как будут выглядеть значения id, и легко создаёт эффект «прыгающих» чисел.
Сначала про имена. Смотрите внимательно на эту конструкцию:
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.SequenceGenerator;
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // generator — это имя генератора В JPA
@SequenceGenerator(name = "category_seq", sequenceName = "category_seq") // sequenceName — это имя sequence В БАЗЕ
private Long id; // Само поле первичного ключа
generator = "category_seq" — это ссылка на имя генератора внутри JPA. То есть это «псевдоним», которым вы будете пользоваться в аннотациях.
sequenceName = "category_seq" — это реальное имя sequence в PostgreSQL. Это уже не псевдоним, а конкретный объект в базе.
Очень часто ошибка выглядит так: в Java написали sequenceName = "categories_seq", а в базе реально существует category_seq. В итоге Hibernate честно пытается вызвать nextval('categories_seq') и получает от PostgreSQL сообщение: «такой sequence не существует».
Теперь про allocationSize. Если вы его не зададите, то по спецификации JPA дефолт обычно не равен 1 — часто это 50. Это означает, что Hibernate может брать числа «пачкой» и раздавать их в памяти. Для производительности это бывает полезно, но для обучения почти гарантированно приводит к вопросу: «Почему id стали 1, потом 51, потом 101… я что, сломал базу?»
Поэтому в учебном проекте мы часто фиксируем allocationSize = 1, чтобы поведение было максимально прозрачным:
import jakarta.persistence.SequenceGenerator;
@SequenceGenerator(
name = "category_seq", // Имя генератора внутри JPA (на него ссылается @GeneratedValue(generator=...)
sequenceName = "category_seq", // Имя sequence в PostgreSQL (то, что реально существует в БД)
allocationSize = 1 // Не берём значения блоками, чтобы не пугаться «скачков» в учебном проекте
)
Соберём это в маленький цельный фрагмент, который удобно копировать в Category и Product:
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
@Id // Первичный ключ сущности
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // Берём значение из sequence
@SequenceGenerator(name = "category_seq", sequenceName = "category_seq", allocationSize = 1) // Прозрачное поведение в учебном проекте
private Long id;
И важное предупреждение: даже с allocationSize = 1 вы всё равно не обязаны увидеть идеальную непрерывную последовательность id без дырок. Это нормально. Ваша цель — уникальность и стабильная идентичность записи, а не эстетика цифр.
7. Выбор стратегии в shop-data-jpa
Когда вы делаете учебный проект, самое опасное — оставить всё «как-нибудь по умолчанию», а потом ловить сюрпризы в логах и считать, что ORM «живёт своей жизнью». Поэтому мы в shop-data-jpa фиксируем явное решение: для PostgreSQL берём GenerationType.SEQUENCE и даём последовательностям понятные имена по шаблону <table>_seq.
Ниже — не целиком сущность, а только фрагмент про генерацию id. Остальная форма класса здесь специально опущена: сейчас нас интересует источник ключа, а не все поля сразу.
Это выглядит примерно так — укороченный, но жизненный фрагмент для Category:
import jakarta.persistence.*;
@Entity // Это JPA-сущность
@Table(name = "category") // Явно фиксируем имя таблицы, чтобы не гадать по неймингу
public class Category {
@Id // Первичный ключ
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // Источник id — sequence
@SequenceGenerator(name = "category_seq", sequenceName = "category_seq", allocationSize = 1) // JPA-имя и имя в БД + прозрачный шаг
private Long id;
}
И аналогично для Product: снова показываем только фрагмент про id.
import jakarta.persistence.*;
@Entity // Это JPA-сущность
@Table(name = "product") // Явно фиксируем таблицу
public class Product {
@Id // Первичный ключ
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq") // Берём id из sequence product_seq
@SequenceGenerator(name = "product_seq", sequenceName = "product_seq", allocationSize = 1) // allocationSize=1 для предсказуемых id в учебном проекте
private Long id;
}
Почему это хороший выбор именно для «учебного, но реалистичного проекта»:
Во‑первых, PostgreSQL очень естественно работает с sequences, и вы легко увидите в SQL-логах связку nextval → insert. Это помогает держать в голове связь «аннотация → SQL».
Во‑вторых, вы получаете единый стиль для всех сущностей каталога. Когда позже в проекте появятся остальные сущности — заказы, позиции и так далее, — вам не придётся каждый раз вспоминать: «а здесь у нас identity или sequence?» Для команды это мелочь, но приятная: единообразие снижает когнитивную нагрузку.
В‑третьих, вы заранее тренируете дисциплину: в базе есть конкретные объекты (category_seq, product_seq), в коде они названы явно, и никакого «угадывания» не происходит. Для новичка это прям спасение: легче дебажить, легче сверять, легче объяснять самому себе.
8. Типичные ошибки при работе с @Id и генерацией ключей
Ошибка №1: использовать long вместо Long и потом удивляться «странному id = 0».
Пока объект ещё не сохранён, id логично «не задан». В Java это выражается через null. У примитива long null нет, поэтому вы получаете 0 и начинаете воспринимать его как реальное значение. Это почти всегда приводит к путанице и плохим решениям в духе «давайте считать 0 признаком нового объекта».
Ошибка №2: одновременно ставить @GeneratedValue и вручную присваивать id.
Это типичный конфликт ответственности: вы сказали JPA «пусть база генерирует», а потом сами назначили значение через setId(...). В результате вы либо получите попытку вставить строку с вашим id и конфликт по PRIMARY KEY, либо словите ситуацию, где ORM ожидает одно, а вы делаете другое.
Ошибка №3: ждать, что id будет строго по порядку и без пропусков.
И IDENTITY, и SEQUENCE могут давать «дырки» в числах — из-за откатов, удалений, параллельной работы, резервирования значений и обычной жизни базы. id — это технический ключ, а не «красивый номер для клиента». Если нужен красивый номер, его делают отдельным полем и с отдельными правилами.
Ошибка №4: перепутать name и sequenceName в @SequenceGenerator.
name — это имя генератора в JPA, sequenceName — имя sequence в базе. Иногда люди меняют одно, забывают поменять другое и получают ошибку «sequence not found», хотя «я же всё назвал почти одинаково». Важно помнить: одно имя — про Java, второе — про PostgreSQL.
Ошибка №5: не контролировать allocationSize и пугаться «скачков» id.
Если Hibernate начнёт выдавать id 1, потом 51, потом 101 — это не поломка базы, а обычная оптимизация, когда идентификаторы берутся блоками. В учебном проекте лучше поставить allocationSize = 1, чтобы поведение было прозрачнее, а в реальном продакшене вы будете выбирать это уже осознанно, а не случайно.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ