1. Від таблиці до класу
Підключений PostgreSQL і налаштований DataSource самі по собі ще не створюють персистентної моделі. Застосунок уже вміє дістатися до бази, але поки не знає, який Java-клас відповідає якій таблиці. Тому наступний крок дуже приземлений: беремо одну таблицю й чесно описуємо клас, який її представлятиме.
Якби розробка була серіалом, сьогодні вийшла б серія: «Зʼявляється перша Entity, і всі роблять вигляд, що розуміють, що відбувається». Наша мета — не вдавати. Ми беремо знайому річ із SQL — таблицю — і робимо так, щоб у Java зʼявився клас, який чесно й явно її представляє, без ілюзії, що Spring Data все прочитає сам.
Уявіть таблицю category. У базі це набір рядків і колонок. У Java ми хочемо отримати обʼєкт Category, у якого поля відображають ці колонки. Щоб Hibernate (як JPA-провайдер) зрозумів: «Ага, це частина персистентної моделі», класу потрібен правильний статус. Саме таким статусом і є @Entity.
Невелика схема, щоб мозок не поїхав у філософію:
flowchart TD
A["Таблиця category у PostgreSQL"] --> B["Метадані відображення JPA"]
B --> C["Клас Category у Java (@Entity)"]
C --> D["Hibernate генерує SQL (INSERT/SELECT)"]
D --> A
Поки ми свідомо тримаємо модель простою: один клас — одна таблиця, поля — прості значення. Без посилань на інші таблиці й без «графів обʼєктів». Спочатку заливаємо бетон, а не намагаємося ставити дах.
2. Анотація @Entity
Іноді здається, що @Entity — це просто ще одна анотація, щоб Spring не сварився. Насправді все серйозніше: без неї клас лишається звичайним Java-класом, і Hibernate його ігнорує. З нею — він потрапляє до persistence unit, і далі вже починається розмова про мапінг і SQL.
Технічно @Entity каже JPA-провайдеру: «Цей клас потрібно включити до моделі, яку ми будемо зберігати в БД». На старті застосунку Hibernate сканує класи й будує метамодель: які є поля, як називається таблиця, як влаштований ключ, якими будуть типи колонок. З цього потім і народжуються SQL-запити, які ви побачите в логах.
Мінімальний приклад — майже «Hello, database»:
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity // Позначаємо клас як JPA-сутність, інакше Hibernate його проігнорує
public class Category {
@Id // Ідентифікатор обовʼязковий: без нього ORM не зможе розрізняти рядки
private Long id;
}
Так, це поки що не надто корисна сутність. Але в ній уже закладено дві ключові ідеї: клас позначено як entity, і в нього є ідентифікатор. Для JPA ідентифікатор — не «опціональна дрібниця», а номер паспорта людини: можна довго міркувати про особистість, але, коли потрібно однозначно зрозуміти, хто це, без номера буде боляче.
Важливо: entity — це не Spring bean. Її не потрібно позначати @Component, @Service чи чимось подібним. Сутність створюється й живе в ORM-механіці, а не в контейнері Spring. Якщо ви спробуєте «впровадити» entity через @Autowired, це буде схоже на спробу «впровадити» рядок таблиці в сервіс — звучить ефектно, сенсу немає.
Де лежить entity у проєкті та як Spring Boot її знаходить
У навчальних проєктах одна з найчастіших проблем звучить так: «Я написав @Entity, але Hibernate ніби її не бачить». І тут починається шаманство: «можливо, версія не та», «можливо, залежність не підключилася», «можливо, Місяць у Козерозі». Насправді причина зазвичай нудна й інженерна: клас лежить не там, де Spring Boot його сканує, або структуру проєкту ускладнили без потреби.
Spring Boot за замовчуванням сканує компоненти й entity відносно пакета, де лежить ваш клас із @SpringBootApplication. Якщо головний клас застосунку знаходиться в com.example.shopdatajpa, то все, що всередині com.example.shopdatajpa.*, зазвичай буде знайдено. Тому для навчального проєкту найздоровіша стратегія — тримати entity всередині кореневого пакета.
Для нашого проєкту ми заздалегідь обрали структуру package-by-feature. Отже, Category і Product мають опинитися тут:
com/example/shopdatajpa/catalog
└─ entity
Це важливо не для краси, а для передбачуваності. Коли проєкт виросте, у вас зʼявляться inventory.entity, ordering.entity, і все лежатиме в зрозумілих місцях. Навіть якщо ви поки працюєте самі, це рятує від ситуації: «Через тиждень я сам не розумію, де що лежить».
Мініприклад «як це виглядає в коді» — з пакетом, щоб потім не було сюрпризів:
package com.example.shopdatajpa.catalog.entity; // Пакет має потрапляти в область сканування Spring Boot
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity // Hibernate побачить цей клас як сутність під час сканування
public class Category {
@Id // Без @Id сутність не вважається коректною JPA-моделлю
private Long id;
}
Якщо ви раптом покладете entity, наприклад, у пакет com.example.entities, а застосунок стартуватиме з com.example.shopdatajpa, то Boot цілком може не побачити цей клас. Виправити це можна по-різному: існує, наприклад, @EntityScan. Але в навчальному проєкті краще лікувати причину, а не симптоми: тримайте структуру простою й передбачуваною.
3. Анотація @Table
З @Entity клас стає учасником персистентної моделі. Але одразу виникає інше питання: «З якою таблицею він повʼязаний?» Тут починається територія домовленостей. JPA й Hibernate вміють виводити імʼя таблиці за замовчуванням. Наприклад, клас Category може перетворитися на таблицю category, а поле createdAt — на колонку created_at. Але ключове слово тут — «може». Багато хто свариться з ORM просто тому, що вони й ORM по-різному розуміють правила неймінгу.
Тому в навчальному проєкті ми практикуватимемо корисну прискіпливість: якщо імʼя таблиці важливе й має бути саме таким, фіксуємо його через @Table. Це прибирає двозначність і особливо допомагає, коли ви читаєте SQL-логи й хочете одразу зрозуміти, до чого належить цей клас.
Приклад із явною привʼязкою:
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity // Оголошуємо: це сутність
@Table(name = "category") // Явно фіксуємо імʼя таблиці, щоб не залежати від правил неймінгу
public class Category {
@Id // Первинний ключ сутності
private Long id;
}
Виглядає як «зайві три рядки». Але насправді це страховка від сюрпризів, особливо коли ви почнете працювати з реальними таблицями, які вже існують і мають заздалегідь задані імена.
Корисно памʼятати: @Entity і @Table відповідають за різні речі. @Entity каже: «цей клас — сутність», а @Table уточнює: «і вона повʼязана ось із цією таблицею». На ранньому етапі краще не покладатися на «воно саме».
Невелика таблиця для закріплення — без спроби стати підручником на тисячу сторінок:
| Що ми описуємо | Анотація | Що фіксує |
|---|---|---|
| «Це сутність» | @Entity | Клас бере участь у персистентній моделі |
| «Ось таблиця» | @Table(name = "...") | Конкретне імʼя таблиці в БД |
4. Мінімальний «скелет» entity
Сутність — не просто «клас із полями». Це клас, з яким ORM має вміти нормально працювати. Тому в нього є кілька базових вимог, і краще дізнатися про них одразу, ніж потім ловити дивні помилки в найнесподіваніший момент. Це радше техніка безпеки, ніж страшна теорія.
По-перше, у entity має бути ідентифікатор — поле з @Id. Ми поки що не обговорюємо, як воно заповнюється: вручну чи автоматично. Зараз важливо зрозуміти інше: без @Id ORM не вміє нормально працювати з обʼєктом як із записом. По-друге, entity має бути звичайним класом, а не record, і їй потрібен конструктор без параметрів. ORM створює обʼєкт рефлексією, і якщо цей шлях перекрити, Hibernate засумує, а сумний Hibernate пише дуже довгі винятки.
Ще один принцип на сьогодні: перший entity-клас має бути максимально простим. Не додавайте туди «на майбутнє» посилання на інші сутності й не намагайтеся заздалегідь проєктувати весь всесвіт. Зараз нам потрібно побачити просту відповідність: таблиця ↔ клас.
Мінімальний приклад «правильної форми»:
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity // Позначаємо клас як сутність
@Table(name = "product") // Явно вказуємо таблицю
public class Product {
@Id // Ідентифікатор рядка таблиці
private Long id;
// Конструктор без параметрів потрібен Hibernate для створення обʼєкта рефлексією
public Product() {
}
}
Так, поки що тут майже порожньо. Але форма вже коректна: Hibernate зможе створити обʼєкт, розпізнати сутність, привʼязати її до таблиці product, а далі ми поступово додамо поля.
5. Перші Category і Product
Зараз ми зробимо те, що часто відкладають надто надовго: спокійно й акуратно накидаємо перші сутності проєкту так, щоб за ними вже було видно, які таблиці в нас є і як вони називаються. За функціональністю це виглядає трохи «бідно», і це нормально: ви будуєте фундамент, а не вітрину інтернет-магазину. У навчальних проєктах важливіші передбачуваність і спостережуваність.
Важливо: фрагменти нижче — не готові фінальні файли сутностей, а перші каркаси. Нам зараз потрібна сама звʼязка таблиця ↔ клас, тому повне налаштування id, колонок і типів ми свідомо не тягнемо в цей шматок.
Category як відображення таблиці category
Зробимо Category у пакеті catalog.entity і відразу закріпимо імʼя таблиці. Поки додамо лише три поля: id, code, name. Тут важливо не те, наскільки це «повна модель», а те, що клас починає відображати сенс таблиці: категорія — це довідник із кодом і назвою.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity // Сутність, яка буде мапитися на таблицю
@Table(name = "category") // Таблиця в БД, з якою повʼязаний клас
public class Category {
@Id // Первинний ключ
private Long id;
// Бізнес-поля: відповідають колонкам таблиці
private String code;
private String name;
}
Якщо ви звикли до ідеї «поля мають бути приватними, а далі — гетери й сетери», так і буде. Але сьогодні ми зосереджуємося на самій звʼязці «клас ↔ таблиця». Деталі про колонки й обмеження (nullable, unique, довжини тощо) — це окрема тема, і не варто змішувати все в одну кашу.
Product як відображення таблиці product
За аналогією створимо Product. Знову ж таки: лише ідентифікатор, sku і name. Жодних цін, статусів та іншого. Наше завдання — закріпити форму.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity // Сутність для ORM
@Table(name = "product") // Явне привʼязування до таблиці product
public class Product {
@Id // Ідентифікатор рядка
private Long id;
// Поля предметної області, які пізніше можна буде уточнювати через @Column тощо
private String sku;
private String name;
}
На цьому етапі дуже корисно подумки тримати в голові SQL-двійника цієї сутності. Не тому, що ми прямо зараз пишемо DDL вручну, а тому, що JPA-світ усе одно будується навколо схеми:
create table product (
id bigint primary key, -- PK відповідає полю з @Id
sku varchar(64), -- приклад колонки для поля sku
name varchar(120) -- приклад колонки для поля name
);
Поки це грубо й без тонкощів, але логіка ясна: обʼєкт стає рядком таблиці.
6. Типові помилки під час мапінгу
Помилка №1: поставити @Entity, але забути @Id.
Новачки іноді думають, що ORM «і так якось розбереться» або що можна почати без ідентифікатора, а потім додати його пізніше. На практиці Hibernate не вміє працювати з сутністю без ключа: він не розуміє, як відрізняти один запис від іншого й як коректно робити оновлення. Тому @Id — обовʼязкова частина скелета.
Помилка №2: сподіватися на «дефолтні імена», не розуміючи правил неймінгу.
Ви назвали клас Category, очікуєте таблицю category, а в логах бачите щось інше — і далі починається магічне мислення. Проблема не в Hibernate, а в очікуваннях. У навчальному проєкті краще відразу закріплювати імʼя таблиці через @Table(name = "..."), щоб не грати в вгадування, особливо поки ви тільки вчитеся читати SQL.
Помилка №3: покласти entity в пакет, який не сканується застосунком.
Це один із найчастіших сценаріїв «чому нічого не працює». Якщо головний клас Spring Boot лежить в одному пакеті, а сутності — в іншому, поза деревом пакетів, Hibernate їх просто не побачить. У результаті таблиці не створюються, мапінг не працює, а ви підозрюєте «зламаний Gradle». Майже завжди це лікується тим, що ви повертаєте класи всередину com.example.shopdatajpa.*.
Помилка №4: намагатися зробити entity «Spring-компонентом».
Іноді хочеться додати @Component або навіть @Service на entity, щоб «Spring точно її знайшов». Але це не потрібно й навіть шкідливо: сутність не призначена для впровадження як залежність. Entity — це модель даних, а не сервіс. Нехай Spring керує сервісами й репозиторіями, а Hibernate — життям обʼєктів усередині ORM-механіки.
Помилка №5: зробити entity record або final-класом, бо «так сучасно».
Сучасно — так. Доречно для JPA entity — здебільшого ні. ORM має мати можливість створити обʼєкт і керувати ним стандартним способом. Якщо ви ускладнюєте форму класу, шанс отримати дивні помилки на рівному місці лише зростає. На старті курсу тримайте entity максимально звичайними Java-класами.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ