JavaRush /Курси /Spring Data JPA /@Id:

@Id: IDENTITY і SEQUENCE

Spring Data JPA
Рівень 5 , Лекція 1
Відкрита

1. Роль @Id у сутності

Якщо @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 справді є стійким ідентифікатором. Але у нашому навчальному міні-магазині ми не будуємо систему навколо зовнішніх ідентифікаторів, тому ручне присвоєння — радше демонстрація того, що так можна, ніж стиль, який варто обирати «за замовчуванням».

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 генерується базою автоматично.)

Найважливіший практичний ефект IDENTITYid стає відомим, коли база виконала 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-логах зв’язок nextvalinsert. Це допомагає тримати в голові зв’язок «анотація → 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, щоб поведінка була прозорішою, а в реальному продакшені ви будете обирати це вже свідомо, а не випадково.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ