JavaRush /Курси /Spring Data JPA /Перша збірка Category

Перша збірка Category і Product

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

1. Що збираємо на практиці

Якщо до цього моменту JPA могла здаватися набором анотацій, то сьогодні ми нарешті побачимо «замкнений цикл»: Java-об’єкт → JPA → SQL → PostgreSQL → назад у Java-об’єкт. Це наш перший маленький «Hello, world!» у новій технології, тільки замість «Hello» у нас цілком доросла річ: запис у таблицю та читання з неї. І так, SQL-лог тут працює як рентген: він показує, що насправді відбувається.

У цій лекції ми робимо просту, але принципову річ: створюємо дві сутності каталогу (Category і Product) без зв’язків між ними. Зв’язки — окрема тема; зараз важливіше навчитися читати базовий мапінг. Потім через EntityManager проганяємо два сценарії: зберегти об’єкт і завантажити його за id. Після цього відкриємо SQL, який реально полетів у базу, і зіставимо його з тим, що написали в коді.

Щоб тримати картину в голові, ось мінісхема того, що відбуватиметься:

flowchart TD
    A[Java-об’єкт Category/Product] --> B[EntityManager.persist]
    B --> C[Згенерований SQL: INSERT + nextval]
    C --> D["PostgreSQL"]
    D --> E[Згенерований SQL: SELECT ... WHERE id = ?]
    E --> F[EntityManager.find]
    F --> G[Java-об’єкт знову в руках]

Головна мета не в тому, щоб «просто запрацювало». Головна мета — щоб ви в будь-який момент могли відкрити лог і сказати собі: «Окей, ось цей INSERT — результат мого @Column, а ось цей SELECT — це мій find».

2. Налаштування dev і SQL-лог

Перед тим як писати код, потрібно домовитися про нудну, але життєво важливу річ: як саме ми побачимо згенерований SQL. Якщо SQL не видно, JPA знову перетворюється на магію. Якщо SQL видно, ви контролюєте ситуацію. Це приблизно як готувати без можливості скуштувати страву: формально можна, але про те, що суп «трохи пересолений», ви дізнаєтеся лише за обличчями гостей.

Ми виходимо з того, що після Дня 4 у вас уже запущено PostgreSQL через Docker Compose і застосунок Spring Boot впевнено підключається до бази. Зараз нам потрібні два режими: у dev ми дозволяємо Hibernate швидко створити таблиці — суто для навчальної швидкості — і вмикаємо логування SQL. Це не про фінальну дисципліну схеми, а саме про навчальний режим: нам важливо побачити INSERT/SELECT і пов’язати їх з анотаціями.

Приклад мінімальних налаштувань для src/main/resources/application-dev.yml:

spring:
  jpa:
    hibernate:
      # Навчальний режим: швидко підіймаємо схему під час старту та очищаємо під час зупинки
      ddl-auto: create-drop
    properties:
      hibernate:
        # Щоб SQL у логах був читабельнішим
        format_sql: true

logging:
  level:
    # Виводить самі SQL-запити
    org.hibernate.SQL: DEBUG
    # Виводить значення, які підставляються замість `?`
    org.hibernate.orm.jdbc.bind: TRACE

Тут три важливі ідеї. По-перше, ddl-auto: create-drop створює таблиці під час старту й видаляє їх під час зупинки, щоб ви не ставили собі питання «чому в мене вже є старі дані» і не думали, що JPA «дублює» записи зі шкідливості. По-друге, org.hibernate.SQL=DEBUG показує самі SQL-запити. По-третє, org.hibernate.orm.jdbc.bind=TRACE показує, які значення підставляються замість ?.

Запускати застосунок зручно так, щоб профіль був увімкнений явно. Наприклад, через змінну середовища:

# Явно вмикаємо профіль dev, інакше не буде потрібних налаштувань і/або таблиць
SPRING_PROFILES_ACTIVE=dev ./gradlew bootRun

Якщо профіль не ввімкнути, ви легко потрапите в ситуацію «таблиць немає» або «SQL не видно», і весь сенс практики зникне. Це не тому, що ви погані, — просто комп’ютер буквально розуміє лише те, що йому сказали.

3. Entity Category

Тепер найприємніше: нижче вже не окремий шматок під одну анотацію, а перша повна версія Category у проєкті. В одному класі зібрані всі вже знайомі частини мапінгу: таблиця, id, контракт полів. Отримуємо робочий файл, який можна одразу зіставляти з SQL-логом. Не намагайтеся зробити Category «ідеальною сутністю на віки»: зараз важливіше, щоб форма була прозорою і читабельною.

Перфекціонізм на цьому кроці зазвичай призводить до того, що студент робить красиво… і нічого не розуміє.

Почнемо з пакета за проєктною структурою: com.example.shopdatajpa.catalog.entity. Файл назвемо Category.java. Одразу використовуємо Jakarta-імпорти, тому що Spring Boot 4 і сучасний стек живуть у світі jakarta.persistence.*, а не javax.persistence.*.

package com.example.shopdatajpa.catalog.entity;

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

@Entity
@Table(name = "category")
public class Category {
}

Поки клас порожній, але дві принципові речі ми вже зробили. @Entity каже JPA: «це частина persistence-моделі». @Table(name="category") фіксує імʼя таблиці та прибирає будь-які здогадки щодо того, як вона має називатися. Для новачка правило «явно краще, ніж неявно» майже завжди працює.

Тепер додамо id і одразу застосуємо ту політику, яку вже зафіксували для проєкту: SEQUENCE, зрозумілу назву sequence і allocationSize = 1. Тут нам не потрібна ще одна велика суперечка IDENTITY проти SEQUENCE; важливо просто зібрати з ухваленого рішення робочий клас.

import jakarta.persistence.SequenceGenerator;

// ...

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq")
@SequenceGenerator(
        name = "category_seq",
        sequenceName = "category_seq",
        allocationSize = 1
)
private Long id;

Тепер додамо поля code, name, description, active. Тут ми закріпимо контракти через @Column: обов’язковість і унікальність для коду, обов’язковість для імені, розумну довжину.

@Column(nullable = false, unique = true, length = 50)
private String code;

@Column(nullable = false, length = 100)
private String name;

@Column(length = 500)
private String description;

@Column(nullable = false)
private boolean active;

І нарешті додамо мінімальний набір конструктора та геттерів/сеттерів. JPA потрібен конструктор без параметрів, а нам тут важливий робочий файл без зайвих трюків.

Нижче — перша повна версія Category, яку вже можна просто скопіювати й зібрати в проєкті:

package com.example.shopdatajpa.catalog.entity;

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

/**
 * Проста сутність для першого циклу persist/find.
 * Без зв’язків, без складної логіки — щоб було легко зіставляти з SQL-логом.
 */
@Entity
@Table(name = "category") // Явно фіксуємо імʼя таблиці в БД
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq")
    @SequenceGenerator(
            name = "category_seq",
            sequenceName = "category_seq",
            allocationSize = 1 // Навчальний режим: передбачувані id без «стрибків»
    )
    private Long id;

    @Column(nullable = false, unique = true, length = 50) // Код обов’язковий і унікальний
    private String code;

    @Column(nullable = false, length = 100) // Імʼя обов’язкове
    private String name;

    @Column(length = 500) // Опис можна не задавати
    private String description;

    @Column(nullable = false) // У базі буде NOT NULL
    private boolean active;

    // Конструктор без параметрів потрібен JPA
    public Category() {
    }

    public Long getId() {
        return id;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }
}

Щоб не потонути в деталях, корисно тримати в голові маленьку таблицю відповідностей. Ми буквально описали, «яка колонка якій відповідає»:

Java-поле SQL-колонка (логічно) Важливий контракт
id: Long id PK, генерується через sequence
code: String code NOT NULL, UNIQUE, довжина до 50
name: String name NOT NULL, довжина до 100
description: String description nullable, довжина до 500
active: boolean active NOT NULL

4. Entity Product і статус

З Product усе так само: це вже не фрагмент під одну анотацію, а перший повний файл сутності. Просто застосовуємо знайомі рішення: ціну тримаємо в BigDecimal, а статус — у enum, який зберігається рядком. Нам зараз важлива не нова теорія типів, а те, як цей вибір перетворюється на робочий мапінг.

Спочатку заведемо enum. Це окремий файл, маленький і приємний, як кошеня (тільки без шерсті в клавіатурі).

package com.example.shopdatajpa.catalog.entity;

public enum ProductStatus {
    ACTIVE,
    ARCHIVED
}

Тепер створимо Product. Почнемо із заготовки: @Entity і @Table(name="product").

package com.example.shopdatajpa.catalog.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity
@Table(name = "product")
public class Product {
}

Додаємо id так само, як у Category, тільки з окремою sequence.

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

// ...

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq")
@SequenceGenerator(
        name = "product_seq",
        sequenceName = "product_seq",
        allocationSize = 1
)
private Long id;

Тепер поля. sku — унікальний і обов’язковий. name — обов’язковий. price — обов’язковий BigDecimal із precision і scale. statusenum з явним EnumType.STRING. Тобто ми просто застосовуємо два вже знайомі правила: гроші не кладемо в double, а статус не зберігаємо ordinal-числом.

Нижче — перша повна версія Product у тому ж стилі:

package com.example.shopdatajpa.catalog.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;

import java.math.BigDecimal;

/**
 * Товар: проста сутність для відпрацювання persist/find і читання SQL-логу.
 */
@Entity
@Table(name = "product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq")
    @SequenceGenerator(
            name = "product_seq",
            sequenceName = "product_seq",
            allocationSize = 1 // Навчальний режим: передбачувані id
    )
    private Long id;

    @Column(nullable = false, unique = true, length = 64) // Унікальний артикул/sku
    private String sku;

    @Column(nullable = false, length = 120) // Назва обов’язкова
    private String name;

    @Column(nullable = false, precision = 12, scale = 2) // Грошове значення: numeric(12,2)
    private BigDecimal price;

    @Enumerated(EnumType.STRING) // У БД зберігаємо рядок ACTIVE/ARCHIVED, а не ordinal
    @Column(nullable = false, length = 20)
    private ProductStatus status;

    // Конструктор без параметрів потрібен JPA
    public Product() {
    }

    public Long getId() {
        return id;
    }

    public String getSku() {
        return sku;
    }

    public void setSku(String sku) {
        this.sku = sku;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public ProductStatus getStatus() {
        return status;
    }

    public void setStatus(ProductStatus status) {
        this.status = status;
    }
}

І знову можна тримати в голові «контракт-таблицю», щоб бачити, що саме ми просимо від бази:

Java-поле SQL-колонка (логічно) Важливий контракт
sku: String sku NOT NULL, UNIQUE, довжина до 64
price: BigDecimal price numeric(12,2), NOT NULL
status: ProductStatus status varchar, NOT NULL, зберігається як рядок

5. persist(...) і INSERT

Час зробити той самий перший цикл: створити об’єкт, зберегти його й побачити в логах SQL. Це найкращий спосіб перестати боятися JPA і почати ставитися до неї з повагою. Тут ми свідомо використовуємо EntityManager напряму, тому що це найчесніший і наймінімальніший варіант: коли ви викликаєте persist(), ви буквально кажете: «зроби так, щоб цей об’єкт став рядком у таблиці». У прикладному коді такі виклики зазвичай ховають за репозиторієм і сервісною транзакцією, але зараз нам потрібен максимально прозорий низькорівневий сценарій.

У Spring Boot нам зручно запустити код автоматично під час старту застосунку. Для цього підійде ApplicationRunner. Щоб він не заважав в інших режимах, повісимо @Profile("dev"): у dev він виконається, в інших профілях — ні. Усередині будемо працювати через EntityManagerFactory, створюючи EntityManager вручну. Це дозволяє явно керувати транзакцією через EntityTransaction і дуже добре вкладається у вашу SQL-картину світу з перших днів.

Створімо клас у пакеті, наприклад, com.example.shopdatajpa.catalog.demo.

package com.example.shopdatajpa.catalog.demo;

import com.example.shopdatajpa.catalog.entity.Category;
import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.entity.ProductStatus;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
@Profile("dev") // Запускаємо демо-код лише в dev-профілі
public class CatalogJpaSandboxRunner implements ApplicationRunner {

    private final EntityManagerFactory emf;

    public CatalogJpaSandboxRunner(EntityManagerFactory emf) {
        this.emf = emf;
    }

    @Override
    public void run(ApplicationArguments args) {
        // 1) Зберігаємо сутності (persist + commit -> INSERT у БД)
        Long categoryId = saveCategory();
        Long productId = saveProduct();

        // 2) Виводимо id, щоб розуміти, що генерація ключів справді спрацювала
        System.out.println("Збережено categoryId=" + categoryId); // Збережено categoryId=1
        System.out.println("Збережено productId=" + productId);   // Збережено productId=1
    }

    private Long saveCategory() {
        // Кожен блок створює окремий EntityManager, щоб було простіше бачити SQL крок за кроком
        try (EntityManager em = emf.createEntityManager()) {
            EntityTransaction tx = em.getTransaction();
            tx.begin(); // Без транзакції INSERT не буде зафіксовано

            Category c = new Category();
            c.setCode("books");
            c.setName("Books");
            c.setDescription("All kinds of books");
            c.setActive(true);

            em.persist(c); // Плануємо INSERT

            tx.commit(); // Фіксуємо транзакцію -> INSERT справді потрапляє до БД
            return c.getId(); // id вже заповнено після persist/commit (залежно від стратегії)
        }
    }

    private Long saveProduct() {
        try (EntityManager em = emf.createEntityManager()) {
            EntityTransaction tx = em.getTransaction();
            tx.begin();

            Product p = new Product();
            p.setSku("BOOK-001");
            p.setName("Spring in Action");
            p.setPrice(new BigDecimal("49.90")); // Для грошей використовуємо BigDecimal і конструктор з рядка
            p.setStatus(ProductStatus.ACTIVE);

            em.persist(p);

            tx.commit();
            return p.getId();
        }
    }
}

На цьому кроці важливо зрозуміти зміст, а не запам’ятати кожен рядок. Ми створюємо EntityManager, відкриваємо транзакцію, створюємо об’єкт, викликаємо persist, комітимо. Якщо ви робили raw SQL на Рівні 3, то за змістом це майже те саме — тільки замість INSERT INTO ... вручну ми просимо JPA зробити це за нас.

Коли ви запустите застосунок, то повинні побачити в логах SQL приблизно таку картину (формат може відрізнятися, але ідея буде рівно та сама):

select nextval('category_seq')
insert into category (active, code, description, name, id) values (?, ?, ?, ?, ?)

select nextval('product_seq')
insert into product (name, price, sku, status, id) values (?, ?, ?, ?, ?)

І десь поруч, якщо ввімкнено bind-лог, — значення, які підставилися замість ?. Це той момент, коли JPA перестає бути міфічною істотою: ви бачите, що вона просто генерує звичайний SQL.

6. find(...) і SELECT ... WHERE id = ?

Збереження — лише половина історії. Друга половина — переконатися, що читання працює так само чесно: JPA має зробити SELECT, витягти колонки й розкласти значення по полях об’єкта. Тут особливо зручно перевірити, що імена колонок, типи та мапінг enum збіглися з очікуваннями. Якщо ви помилилися в @Column або забули @Enumerated(EnumType.STRING), саме під час читання проблема часто й спливає.

Щоб find(...) точно виконав SQL-запит, зручно робити читання окремим кроком і в окремому EntityManager. Тоді це буде саме «чисте читання з бази», а не ситуація «об’єкт і так уже в пам’яті, тому JPA нічого не робить». Ми додамо в той самий runner методи завантаження та пару println, щоб побачити реальні значення.

Допишемо в CatalogJpaSandboxRunner ще два методи й розширимо run():

@Override
public void run(ApplicationArguments args) {
    Long categoryId = saveCategory();
    Long productId = saveProduct();

    Category loadedCategory = load(Category.class, categoryId);
    Product loadedProduct = load(Product.class, productId);

    System.out.println("Код завантаженої категорії=" + loadedCategory.getCode()); // Код завантаженої категорії=books
    System.out.println("SKU завантаженого товару=" + loadedProduct.getSku());     // SKU завантаженого товару=BOOK-001
    System.out.println("Статус завантаженого товару=" + loadedProduct.getStatus()); // Статус завантаженого товару=ACTIVE
}

private <T> T load(Class<T> type, Long id) {
    try (EntityManager em = emf.createEntityManager()) {
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        T entity = em.find(type, id);

        tx.commit();
        return entity;
    }
}

У SQL-логові ви повинні побачити запити читання, дуже схожі на те, що ми писали вручну на Рівні 2:

select c.id, c.active, c.code, c.description, c.name
from category c
where c.id = ?

select p.id, p.name, p.price, p.sku, p.status
from product p
where p.id = ?

І знову ж таки: якщо ввімкнено bind-лог, ви побачите, що замість ? підставився ваш id.

Ця частина особливо важлива психологічно. Багато новачків думають: «JPA ж щось там зберігає, може, воно взагалі не в базу пише?» А тут ви бачите: ні, усе чесно. Був INSERT. Потім був SELECT. Це рівно те, чого ви й хотіли.

7. Усвідомлене читання SQL-логу

SQL-лог дуже легко перетворити на шум: «там багато рядків, я нічого не зрозумів, отже JPA працює». Нам потрібен зворотний підхід: лог — це інструмент перевірки. Тому корисно заздалегідь домовитися, що саме ви шукаєте в SQL і як пов’язуєте це з мапінгом. Це як із касовим чеком: пам’ятати кожну цифру не потрібно, але корисно помічати, якщо хліб чомусь пробили як телевізор.

По-перше, у sequence-генерації зазвичай буде окремий крок — запит наступного значення sequence. У PostgreSQL це виглядає як select nextval('...'). Важливо розуміти, що це не зайвий запит із примхи, а реальна механіка: база видає наступне значення, JPA кладе його в id, а потім робить INSERT. Коли ви бачите nextval, можете спокійно сказати: «ага, @GeneratedValue(SEQUENCE) працює».

По-друге, в INSERT ви порівнюєте список колонок і порядок значень. Наприклад, якщо бачите, що в category вставляються code, name, description, active, — чудово. Якщо раптом зникло поле active або воно називається дивно, це привід перевірити @Column(name="...") або naming strategy.

По-третє, ви перевіряєте, що обов’язкові поля справді вставляються в межах логіки NOT NULL. Прямо зараз ми не обговорюємо constraints на рівні бази детально, але вже сьогодні можна зловити просту помилку: наприклад, якщо ви не встановили name, а @Column(nullable=false) стоїть, при вставці ви отримаєте помилку. Це корисний зворотний зв’язок: «моя модель справді потребує даних».

По-четверте, ви перевіряєте enum. Якщо в bind-логах бачите значення ACTIVE як рядок, значить EnumType.STRING спрацював. Якщо побачили число 0, значить enum зберігається як ordinal, і це привід зупинитися та виправити. Зараз це ще легко; потім у живій базі це стає дорогою розвагою.

Нарешті, ви перевіряєте BigDecimal. У логах ви побачите щось на кшталт 49.90. Це нормально й очікувано. Якби ціна зберігалася в double, ви могли б побачити значення з «хвостом», і це той випадок, коли комп’ютер не помилився — він просто чесно зробив те, що ви попросили.

8. Перевірка даних SQL-запитом

Після того як JPA-сценарій відпрацював, корисно зробити коротку «перевірку реальності»: відкрити будь-яку SQL-консоль — psql, DBeaver, DataGrip, що завгодно — і виконати простий SELECT. Це не тому, що ми не довіряємо Hibernate. Це тому, що інженерна звичка «перевіряй факт» рятує від величезної кількості майбутніх багів. Плюс це чудовий міст між SQL-мисленням і ORM.

Наприклад, можна виконати:

select id, code, name, active
from category
order by id;

І очікувати побачити приблизно такі дані:

--  id | code  | name  | active
-- ----+-------+-------+--------
--   1 | books | Books | true

А для товарів:

select id, sku, name, price, status
from product
order by id;

Приклад очікуваного результату за змістом:

--  id | sku      | name            | price | status
-- ----+----------+-----------------+-------+---------
--   1 | BOOK-001 | Spring in Action| 49.90 | ACTIVE

Зверніть увагу на дві речі. По-перше, ви бачите, що в колонці status справді зберігається рядок ACTIVE, а не число. По-друге, ціна лежить як нормальне десяткове значення з двома знаками. Це саме те, що ми «замовили» через precision/scale.

Так, у навчальному режимі з create-drop дані зникнуть після зупинки застосунку — і це нормально. Мета зараз не «накопичити базу», а побачити, що модель і SQL збігаються.

9. Типові помилки під час persist/find

На першій практиці майже всі помиляються — це нормально. Важливо не «не помилятися», а навчитися розпізнавати помилку за симптомами і розуміти, де саме вона живе: в анотаціях, у конфігу, в типах полів чи просто в тому, що застосунок запущено не тим профілем. Нижче — набір граблів, на які настають найчастіше. І майже всі вони чудово лікуються читанням SQL-логу.

Помилка №1: імпортовано javax.persistence.* замість jakarta.persistence.*.
У сучасному Spring Boot 4 ви живете в Jakarta-світі. Якщо за звичкою або за старою статтею імпортувати javax.persistence.Entity, IDE може підсвічувати дивні помилки, а проєкт — узагалі не зібратися. Симптом зазвичай простий: компіляція падає або анотації «не знаходяться». Лікування — замінити імпорти на jakarta.persistence.* і перестати відкривати туторіал 2016 року як джерело істини.

Помилка №2: забули ddl-auto або запустили без профілю dev, і таблиць немає.
Класичний симптом — під час першого INSERT ви отримуєте від PostgreSQL повідомлення на кшталт «relation category does not exist». JPA тут ні до чого: база чесно каже «такої таблиці немає». Перевірте, що активовано dev-профіль і що в ньому ввімкнено режим генерації схеми — у межах навчальної практики.

Помилка №3: забули @Entity або забули @Id.
Якщо забути @Entity, Hibernate не сприйматиме клас як сутність, і далі посиплються помилки мапінгу або сканування. Якщо забути @Id, помилка зазвичай звучить ще прямолінійніше: JPA не вміє працювати із сутністю без ідентифікатора. Це як спробувати вести каталог товарів без унікального «внутрішнього номера» — ви просто не зможете відрізнити один товар від іншого.

Помилка №4: не вказали allocationSize = 1, і id виглядають «стрибучими».
Це не баг, а наслідок оптимізації, але для першої практики виглядає лячно: «чому в мене товари отримують id 1, 51, 101?» Якщо ви хочете максимально спокійний навчальний режим, фіксуйте allocationSize = 1. І при цьому не робіть із id бізнес-значення та не чекайте, що він обов’язково має бути «красивим» і безперервним.

Помилка №5: ціну зроблено double, і в базі або логах з’являються дивні дроби.
Якщо ви десь замінили BigDecimal на double «для простоти», ви майже гарантовано отримаєте історію про 49.900000000000006. Це не «зламалася база». Це типова проблема двійкових дробів у floating-point. Для грошей у persistence-моделі — BigDecimal, і краще одразу у вигляді рядка new BigDecimal("49.90"), як ми й зробили.

1
Опитування
JPA-сутності, рівень 5, лекція 4
Недоступний
JPA-сутності
Анотації, ключі, генерація ID
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ