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[Generated SQL: INSERT + nextval]
    C --> D["PostgreSQL"]
    D --> E[Generated 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("Saved categoryId=" + categoryId); // Saved categoryId=1
        System.out.println("Saved productId=" + productId);   // Saved 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("Loaded category code=" + loadedCategory.getCode()); // Loaded category code=books
    System.out.println("Loaded product sku=" + loadedProduct.getSku());     // Loaded product sku=BOOK-001
    System.out.println("Loaded product status=" + loadedProduct.getStatus()); // Loaded product status=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
Задача
Spring Data JPA, 5 уровень, 4 лекция
Недоступна
Сохранение и чтение `Category` через `EntityManager`
Сохранение и чтение `Category` через `EntityManager`
1
Задача
Spring Data JPA, 5 уровень, 4 лекция
Недоступна
Сохранение и чтение `Product` с `BigDecimal` и enum
Сохранение и чтение `Product` с `BigDecimal` и enum
1
Опрос
JPA Сущности, 5 уровень, 4 лекция
Недоступен
JPA Сущности
Аннотации, ключи, генерация ID
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ