JavaRush /Курсы /Spring Data JPA /Идентичность entity и состояния

Идентичность entity и состояния

Spring Data JPA
6 уровень , 0 лекция
Открыта

1. Смысл состояний entity

Когда новичок впервые пишет JPA-код, он обычно думает так: «У меня есть объект. Я поменял поле. Значит, база должна обновиться». Это звучит логично… пока вы не узнаете, что JPA не телепат. Чтобы ORM “увидела” изменения и связала их с конкретной строкой в таблице, объект должен находиться в правильном состоянии относительно JPA. Поэтому сегодня мы вводим простую, но мощную идею: entity живёт в разных состояниях, и эти состояния объясняют почти все ранние «почему оно так себя ведёт?!».

Вам важно поймать правильную интуицию: JPA не следит за каждым объектом JVM, который вы когда-либо создали. Она следит только за теми объектами, которые в данный момент находятся у неё “на учёте”. И “учёт” здесь — не бухгалтерия (хотя иногда ощущается именно так), а технический факт: видит ли JPA этот объект и ассоциирует ли его с записью в БД.

2. Идентичность entity

Не просто «мешок полей»

Если упростить до одной фразы, entity — это Java-объект, который представляет конкретную запись в базе данных (или потенциальную запись, если он ещё не сохранён). И вот здесь начинается то, что многие чувствуют интуитивно, но редко формулируют: у объекта в Java и у строки в базе — разная “природа идентичности”.

В Java по умолчанию идентичность объекта — это, грубо говоря, “тот ли это самый экземпляр в памяти” (сравнение по ссылке, ==). В базе данных идентичность строки — это “та же ли это запись” (обычно через PRIMARY KEY). В JPA миры встречаются: мы хотим мыслить объектами, но база всё равно живёт строками и ключами.

Когда вы создаёте новый Product через new Product(), вы получаете объект с полями, но без database-идентичности: у него либо нет id, либо оно ещё не назначено. Такой объект ещё не привязан к конкретной строке таблицы product, потому что строки пока нет. И вот это состояние “у объекта ещё нет жизни в БД” — центральная часть сегодняшней лекции.

Для нашего проекта shop-data-jpa это выглядит максимально приземлённо: sku — это бизнес-смысл товара (в реальности он уникален), но техническая идентичность в базе обычно выражена через id. И JPA ориентируется именно на id, когда “узнаёт” сущность. Бизнес-ключи (вроде sku) мы обязательно обсудим подробнее позже, когда будем говорить про equals() и hashCode(), но уже сейчас полезно помнить: у сущности есть “техническая” идентичность (PK) и “смысловая” (бизнес-ключ).

Мини-кусочек нашей сущности (для ориентира, не полный файл):

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity // Говорим JPA, что этот класс — сущность (маппится на таблицу)
public class Product {

    @Id // Техническая идентичность сущности: по этому полю JPA "узнаёт" запись
    private Long id;

    @Column(nullable = false, unique = true) // Бизнес-ключ: важен для смысла, но не равен "управлению JPA"
    private String sku;
}

Даже в этом маленьком фрагменте видно: sku важен для бизнеса, но JPA начинает “разговор” о сущности именно с @Id.

3. Базовые состояния entity

transient: объект вне JPA

Состояние transient проще всего понять человечески: это обычный новый Java-объект, созданный через new, который пока не является частью persistence-модели. Он живёт в памяти, он может быть красивым, заполненным и даже очень уверенным в себе… но для базы данных его не существует. Это как заполнить анкету, но не нажать кнопку “Отправить”: данные есть, а в системе — нет.

Самая частая ошибка на этом этапе — ожидать от transient-объекта поведения “как у сохранённой сущности”. Например, ждать, что у него уже есть id, или думать, что если вы поменяли поле, то JPA “как-то это увидит”. У transient объекта нет никакой связи с БД. Он может стать entity в полном смысле слова только после того, как мы сделаем его известным JPA.

Мини-пример transient-объекта в нашем домене:

import com.example.shopdatajpa.catalog.entity.Product;
import java.math.BigDecimal;

Product product = new Product(); // transient: просто объект, JPA о нём не знает
product.setSku("PHONE-1");       // изменения только в памяти JVM
product.setName("Phone");
product.setPrice(new BigDecimal("499.00"));

Здесь product уже существует как объект, но пока это просто объект. Даже если он выглядит “как товар”, в БД он ещё не появился. И самое важное: JPA пока не обязана (и не может) отслеживать его изменения, потому что вы ещё не сказали ей: “вот эта штука — часть persistence-модели”.

managed: под опекой JPA

Состояние managed — это момент, когда JPA говорит: “Окей, я знаю этот объект. Он мой. Я буду считать его частью текущей работы с базой”. У managed entity появляется очень важное свойство: JPA может отслеживать изменения полей (именно “может”, без деталей “когда и как улетит SQL” — это будет позже, сейчас нам нужна интуиция). Поэтому один и тот же вызов setName(...) вдруг начинает иметь другой смысл: это уже не просто изменение в памяти, это изменение, которое JPA потенциально учитывает в рамках работы с БД.

Как entity становится managed? На базовом уровне есть два понятных пути. Первый путь — вы загрузили сущность из базы через JPA, и она автоматически становится managed. Второй путь — вы попросили JPA сохранить новый объект, и он тоже становится managed, потому что JPA должна “вести” его как часть операции.

Самый простой способ почувствовать managed-состояние — через EntityManager.find(...):

import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;

Product product = entityManager.find(Product.class, 1L); // managed: сущность загружена и отслеживается
product.setName("Updated name"); // теперь JPA видит изменение (entity managed)

Ключевая мысль: find() не просто возвращает объект. Он возвращает объект, который JPA считает “подконтрольным” в рамках текущей работы. Если очень грубо, EntityManager — это “центр управления полётами” для сущностей: через него сущности попадают в зону видимости JPA.

Если хочется пример с “рождением” managed из transient (идея, без попытки сделать сейчас полноценный production-код), он выглядит так:

import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;
import java.math.BigDecimal;

Product product = new Product(); // transient: пока просто объект
product.setSku("PHONE-1");
product.setName("Phone");
product.setPrice(new BigDecimal("499.00"));

entityManager.persist(product); // managed: JPA начинает управлять entity
// Важно: SQL INSERT может выполниться не прямо тут (зависит от flush/транзакции) — это отдельная тема

Смысл persist() в контексте сегодняшней лекции простой: JPA теперь знает объект и держит его как часть persistence-модели. Мы не разбираем, когда именно будет выполнен SQL INSERT и как именно это связано с транзакцией и синхронизацией — это отдельная большая тема, которая методически будет позже. Сейчас нас интересует именно переход: “был просто объект → стал управляемой сущностью”.

detached: объект без отслеживания

detached — это состояние, которое чаще всего ломает ожидания, потому что “по ощущениям” всё выглядит почти так же, как в managed: объект у вас в переменной, поля доступны, геттеры и сеттеры работают, никакой ошибки прямо сейчас не возникает. Но есть одно принципиальное отличие: JPA больше не следит за изменениями этого объекта. То есть вы можете менять поля сколько угодно, а для базы данных это не будет иметь никакого эффекта — потому что JPA об этом не знает (и знать не обязана).

Как сущность становится detached? Концептуально так: она была известна JPA (например, загружена из БД), но затем “вышла из зоны видимости” текущей работы JPA. Это может происходить по разным причинам, но нам сейчас важно уловить базовый факт: detached — это “бывшая managed”. Она уже была частью persistence-модели, но сейчас стала обычным объектом без автоматического сопровождения со стороны ORM.

С точки зрения EntityManager можно буквально “отцепить” сущность:

import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;

Product product = entityManager.find(Product.class, 1L); // managed: JPA следит за сущностью
entityManager.detach(product);        // detached: сущность "отцепили" от контекста
product.setName("Changed later");     // JPA уже не следит за этим изменением

Очень важно не путать transient и detached. transient — это “ещё никогда не был частью БД”. detached — это “уже был частью БД, но сейчас не под контролем JPA”.

И да, здесь легко поймать философскую мысль: detached-объект всё ещё может иметь id, потому что он был сохранён ранее. Но наличие id не делает его автоматически managed. id — это признак того, что объект может соответствовать строке в БД, но управление этим соответствием — отдельная история.

removed: помечено на удаление

Состояние removed — ещё одна ловушка для новичка. Часто кажется, что удалить сущность — это “ну я же больше не хочу этот объект, значит он должен исчезнуть”. И тут хочется написать что-то вроде product = null; и почувствовать себя победителем. Но null всего лишь означает: “у меня больше нет ссылки в этой переменной”. Это вообще не разговор с базой данных. База даже не в курсе, что вы там пережили маленькую драму разрыва отношений с объектом.

removed в JPA означает другое: сущность помечена на удаление как часть текущей работы с базой. Важно именно слово “помечена”: это состояние внутри механики ORM, а не мгновенное исчезновение всего на свете.

Мини-пример:

import com.example.shopdatajpa.catalog.entity.Product;
import jakarta.persistence.EntityManager;

Product product = entityManager.find(Product.class, 1L); // managed
entityManager.remove(product); // removed: JPA пометила запись на удаление
System.out.println(product.getSku()); // объект в памяти жив и доступен (это не GC и не null)

Здесь можно заметить аккуратную “инженерную разницу”: удаление — это не GC и не “стереть объект из оперативки”. Это намерение в рамках persistence-операции: “в базе эту запись нужно удалить”. А объект как Java-объект может существовать дальше — просто JPA уже относится к нему иначе.

И снова тот же мотив дня: одинаковые на вид действия (set..., чтение полей, присваивание переменной) имеют разные последствия в зависимости от состояния сущности.

4. Карта состояний

Когда терминов становится больше двух, мозг начинает просить: “Можно, пожалуйста, карту местности? Я ещё не готов быть компасом”. Это нормальная реакция. Поэтому сейчас мы соберём всё в один компактный “атлас”: что означает каждое состояние, как в него попасть и что ждать от JPA.

В виде таблицы это выглядит так:

Состояние Что это по-человечески Откуда берётся JPA отслеживает изменения? Есть связь с БД?
transient новый объект в памяти new Product() нет нет (записи может не быть)
managed объект “под контролем” JPA find(...), persist(...) да да (или будет создана)
detached объект “отцепился” от JPA detach(...) или выход из контекста нет да (обычно уже есть запись)
removed объект помечен на удаление remove(...) JPA ведёт его как удаляемый да (запись будет удаляться)

Если хочется увидеть это как “переходы состояний”, можно представить такую схему. Она не пытается быть справочником всего JPA (мы сознательно держим минимум), но отлично фиксирует смысл:

stateDiagram-v2
    %% Базовая "карта" переходов: это не полный справочник по JPA, а минимальная шпаргалка
    [*] --> Transient: "new"
    Transient --> Managed: "persist(...)"
    Managed --> Detached: "detach(...)"
    Managed --> Removed: "remove(...)"

Обратите внимание, что здесь нет некоторых методов и состояний, которые вы встретите позже в курсе (и это нормально). Сегодня нам важно, чтобы вы уверенно различали базовую четвёрку и не думали, что “entity — это просто объект”. В нашем проекте shop-data-jpa это знание прямо сейчас поможет вам трезво оценивать, почему в одном месте изменения “подхватываются”, а в другом — нет, даже если вы пишете одинаковые сеттеры.

Если хотите совсем короткое правило на память, оно звучит так: managed — это “JPA видит”, detached/transient — “JPA не видит”, removed — “JPA видит и собирается удалить”. Состояния — это не “теория ради теории”, это язык, на котором вы объясняете поведение persistence-модели.

5. Типичные ошибки при работе с состояниями entity

Ошибка №1: считать, что любой Product автоматически “живёт в базе”, раз на классе стоит @Entity.
Аннотация делает класс сущностью с точки зрения маппинга, но не делает каждый экземпляр этого класса управляемым. new Product() — это просто объект. Пока вы не загрузили его через JPA или не сделали его “известным” ORM, он остаётся transient.

Ошибка №2: путать transient и detached, потому что “и там, и там JPA не следит”.
Это реально похожие ощущения, но смысл разный. transient — “ещё никогда не был частью persistence-модели”, обычно без id. detached — “уже был, но сейчас отцепился”, часто с id. Это различие потом будет критично, когда вы начнёте обновлять данные и удивляться, почему одно обновляется, а другое нет.

Ошибка №3: ожидать, что вызов setName(...) сам по себе “обновляет базу”, независимо от состояния.
Сеттер меняет поле объекта, и это всегда работает на уровне Java. Но эффект для базы зависит от того, managed объект или нет. Если объект detached, вы можете хоть десять раз вызвать setName(...), база не “обязана” что-то узнать об этом. Для базы вы просто поменяли поле у объекта в памяти — и всё.

Ошибка №4: думать, что removed — это “объект исчез” и после remove() к нему нельзя обращаться.
removed — это про намерение удалить запись в БД, а не про уничтожение объекта в памяти. Java-ссылка остаётся ссылкой, объект остаётся объектом, и его поля доступны. Просто теперь JPA считает этот объект помеченным на удаление, а не обычным “живым” entity.

Ошибка №5: подменять удаление из базы удалением ссылки (product = null;).
null влияет только на вашу переменную. База данных не читает ваши мысли и не смотрит в ваш heap (и слава богу). Если нужно удалить строку в таблице, это делается через JPA-операцию удаления, а не через “обнуление”.

1
Задача
Spring Data JPA, 6 уровень, 0 лекция
Недоступна
Смена состояний `DemoProduct`
Смена состояний `DemoProduct`
1
Задача
Spring Data JPA, 6 уровень, 0 лекция
Недоступна
Состояние `removed` без исчезновения Java-объекта
Состояние `removed` без исчезновения Java-объекта
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ