сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Spring и Hibernate

Состояния entity в Hibernate

Состояния объекта: почему база меняется без save

Загрузил автора, присвоил новое имя полю и закрыл транзакцию. Никакого сохранения я не вызывал. В следующей транзакции имя в базе оказалось новым.

Это не магия, а поведение по умолчанию: пока объект связан с сессией, библиотека отображения объектов на таблицы (ORM, object-relational mapping) сама замечает изменения и пишет их. Понимать это надо в обе стороны - и чтобы не искать пропавший save, и чтобы случайная правка поля не уехала в базу.

// Формулировки: «какие состояния бывают у сущности?», «что такое persistence context?», «чем persist отличается от merge?», «когда происходит flush?».

Четыре состояния и контекст персистентности

Сессия - это разговор с базой в рамках одной операции, и у него есть память: контекст персистентности, куда складываются все загруженные объекты. В любой момент объект находится в одном из четырёх состояний.

Transient - только что созданный через new, базе о нём неизвестно. Persistent (управляемый) - находится в контексте: любое изменение поля будет замечено. Detached (отсоединённый) - когда-то был управляемым, но сессия закрылась; в базе строка есть, а связи с ней уже нет. Removed - помечен на удаление.

// Отсюда LazyInitializationException, самая частая ошибка новичка. Я взял объект, закрыл сессию и обратился к его ленивой коллекции - получил ровно это исключение. Объект стал отсоединённым, подгружать данные некому: разговор с базой закончился.

контекст персистентности
память сессии: все объекты, загруженные в её рамках
detached
объект, переживший свою сессию: изменения больше не отслеживаются

Автоматическое отслеживание изменений

Механизм называется dirty checking - «проверка на грязное». При загрузке объекта ORM запоминает снимок всех его полей. Перед записью в базу она сравнивает текущее состояние со снимком и генерирует UPDATE для тех полей, что разошлись. Именно поэтому мой автор переименовался без единого вызова save.

Момент записи называется flush - выталкивание накопленных изменений в базу. Происходит он в трёх случаях: перед коммитом транзакции, перед выполнением запроса, который может задеть изменённые данные, и по явному вызову. Важно различать: flush отправляет команды в базу, а commit фиксирует их окончательно. Между этими событиями изменения уже видны внутри транзакции, но их ещё можно откатить.

// Практическое следствие: держать управляемый объект дольше, чем нужно, опасно. Случайно присвоенное поле уедет в базу само. И обратное: если ты изменил ОТСОЕДИНЁННЫЙ объект, не изменится ничего - его никто не отслеживает.

dirty checking
сравнение объекта со снимком при загрузке, чтобы найти изменения
flush / commit
отправить накопленные команды в базу / зафиксировать транзакцию

persist, merge и кэш первого уровня

persist берёт новый объект и делает его управляемым: ТОТ ЖЕ объект получает идентификатор. merge работает с отсоединённым: он не присоединяет твой объект обратно, а копирует его состояние в управляемую копию и ВОЗВРАЩАЕТ её. Поэтому после merge надо работать с возвращённым значением - забыть про это классическая ошибка: правишь старую ссылку, а изменения уходят в никуда.

Сам контекст работает как кэш. Я трижды запросил объект по одному идентификатору в рамках одной сессии: к базе ушёл ОДИН запрос, а все три раза вернулся один и тот же объект в памяти - сравнение по ссылке дало true.

// Это даёт удобную гарантию: в пределах одной сессии одна строка базы представлена ровно одним объектом. Поэтому сравнение таких объектов по == внутри сессии работает - и поэтому же оно перестаёт работать, стоит выйти за её пределы.

var a1 = s.find(Author.class, 1L);
var a2 = s.find(Author.class, 1L);
var a3 = s.find(Author.class, 1L);
// это один и тот же объект: true
// запросов к базе: 1
persist / merge
сделать новый объект управляемым / скопировать состояние отсоединённого

Как отвечать: «Почему объект обновился в базе, хотя я не звал save?»

Потому что он был управляемым, то есть находился в контексте сессии. ORM при загрузке делает снимок всех полей объекта, а перед записью сравнивает текущее состояние со снимком и сама генерирует UPDATE по разошедшимся полям - это называется dirty checking. Я это проверял: загрузил объект, присвоил полю новое значение, никакого сохранения не вызывал, и в следующей транзакции значение в базе было новым. Выталкивание команд происходит перед коммитом, а также перед запросом, который может задеть изменённые данные. Отсюда практический вывод в обе стороны: случайно тронутое поле управляемого объекта уедет в базу само, поэтому такие объекты не стоит держать дольше нужного и не стоит использовать как обычные структуры данных. А вот у отсоединённого объекта правка полей не сделает ничего - его никто не отслеживает.

Почему это сильный ответ: назван механизм со снимком полей, момент записи и оба практических следствия - и опасность, и обратный случай, где люди наоборот ждут сохранения и не получают его.

На чём валят

  • Искать пропавший save. Управляемый объект сохраняется сам, а вот отсоединённый - нет.
  • Обратиться к ленивой связи после закрытия сессии. Это LazyInitializationException, я его получал ровно так.
  • Работать после merge со старой ссылкой. Управляемая копия это то, что merge ВЕРНУЛ, а не то, что ему передали.
  • Держать управляемый объект в долгоживущей структуре. Любая случайная правка поля уедет в базу.
  • Считать, что flush это commit. Команды ушли в базу, но до коммита их ещё можно откатить.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 15, остальные разбираются в тренажёре.

  1. #entity_lifecycle1 / 5
    Что такое контекст персистентности (persistence context) и кэш первого уровня?
    A)Глобальный кэш, разделяемый всеми сессиями и потоками приложения на всё время его работы
    B)Область в рамках сессии, где хранятся managed-сущности; гарантирует их идентичность
    C)Файл на диске, куда Hibernate сбрасывает все загруженные сущности между перезапусками приложения
    D)Пул соединений с базой данных, из которого сессии берут подключение для выполнения запросов
    показать ответ и разбор
    +B)Область в рамках сессии, где хранятся managed-сущности; гарантирует их идентичность

    // разбор: Контекст персистентности (у Hibernate — Session, в JPA — EntityManager) хранит все managed-сущности в пределах одной единицы работы. Это и есть кэш ПЕРВОГО уровня (L1): он ВСЕГДА включён и локален для сессии. Следствия: повторный find по тому же id вернёт ТОТ ЖЕ экземпляр (гарантия идентичности, repeatable read внутри сессии) без второго SELECT; изменения managed-сущностей отслеживаются (dirty checking) и сбрасываются при flush. При закрытии сессии контекст очищается, а сущности становятся detached. L1 нельзя отключить.

  2. #entity_lifecycle2 / 5
    Как Hibernate узнаёт, что managed-сущность изменилась, без явного вызова save?
    A)Он не узнаёт: чтобы сохранить изменения managed-сущности, нужно явно вызвать save/update
    B)Он перехватывает каждое присваивание поля через рефлексию и сразу шлёт UPDATE в базу немедленно
    C)Через dirty checking: сравнивает состояние с снимком и генерирует UPDATE при flush
    D)Он требует, чтобы каждое изменяемое поле было помечено аннотацией @Dirty для отслеживания вручную
    показать ответ и разбор
    +C)Через dirty checking: сравнивает состояние с снимком и генерирует UPDATE при flush

    // разбор: Пока сущность MANAGED, Hibernate хранит снимок её исходного состояния. При flush (перед запросом/коммитом) он сравнивает текущее состояние со снимком (dirty checking) и генерирует UPDATE ТОЛЬКО для реально изменившихся сущностей/полей — явный save вызывать не нужно (это удивляет новичков: «я не сохранял, а в БД изменилось»). Обратная сторона: случайное изменение managed-сущности молча уйдёт в БД. Поэтому важны границы транзакции, readOnly для запросов (отключает dirty checking) и работа с DTO там, где менять не нужно.

  3. #entity_lifecycle3 / 5
    Чем persist отличается от merge?
    A)Persist и merge делают одно и то же; merge оставлен лишь для совместимости со старым кодом JPA
    B)Persist сохраняет только в кэш, а merge — только в базу; для полного сохранения нужны оба вызова
    C)Merge удаляет сущность из контекста, а persist добавляет — это операции с противоположным смыслом
    D)persist — для новой (transient) сущности; merge — вернуть detached в контекст (копией)
    показать ответ и разбор
    +D)persist — для новой (transient) сущности; merge — вернуть detached в контекст (копией)

    // разбор: persist рассчитан на TRANSIENT-сущность (новую): делает её managed, при flush — INSERT. merge работает с DETACHED-сущностью (например, пришедшей из прошлой сессии/по сети): он НЕ делает переданный объект managed, а КОПИРУЕТ его состояние в managed-экземпляр (загрузив его при нужде) и ВОЗВРАЩАЕТ этот управляемый объект — работать дальше надо с возвращённым, а не с исходным. Частая ошибка — вызвать merge и продолжить менять исходную detached-сущность, ожидая сохранения. persist к detached бросит исключение.

  4. #entity_lifecycle4 / 5
    Что делает flush() и отличается ли он от commit транзакции?
    A)flush синхронизирует контекст с БД (шлёт SQL), но НЕ коммитит транзакцию
    B)Flush и commit — синонимы: оба немедленно и окончательно фиксируют изменения в базе данных без отката
    C)Flush очищает кэш первого уровня, удаляя из контекста все загруженные ранее сущности сессии
    D)Flush откатывает все несохранённые изменения сущностей, возвращая их к состоянию на начало транзакции
    показать ответ и разбор
    +A)flush синхронизирует контекст с БД (шлёт SQL), но НЕ коммитит транзакцию

    // разбор: flush() СИНХРОНИЗИРУЕТ контекст персистентности с БД: отправляет накопленные SQL-команды (INSERT/UPDATE/DELETE от dirty checking) на сервер — но в рамках ТЕКУЩЕЙ транзакции, которая ещё НЕ закоммичена (изменения можно откатить). commit фиксирует транзакцию окончательно (и обычно сам вызывает flush перед этим). Hibernate флашит автоматически: перед выполнением запроса (чтобы он видел свежие данные) и перед commit. Понимание flush объясняет, почему исключения БД (constraint) могут вылететь в неожиданный момент — на flush, а не на строке setField.

  5. #entity_lifecycle5 / 5
    В каком состоянии окажется сущность после закрытия сессии/транзакции?
    A)Removed — закрытие сессии автоматически удаляет из базы все загруженные в ней сущности
    B)Detached — она больше не отслеживается контекстом
    C)Transient — сущность теряет свой id и становится как только что созданная через new
    D)Persistent — сущность продолжает отслеживаться и после закрытия сессии, до конца работы приложения
    показать ответ и разбор
    +B)Detached — она больше не отслеживается контекстом

    // разбор: Когда сессия/EntityManager закрывается (или контекст очищается clear/detach), все managed-сущности переходят в DETACHED: у них сохраняются данные и id, но контекст их больше НЕ отслеживает — изменения полей уже НЕ уходят в БД сами, а обращение к ЛЕНИВЫМ, не загруженным ранее связям бросает LazyInitializationException. Чтобы снова сохранять изменения detached-объекта, его возвращают в контекст через merge. Это ядро частых багов: «поменял объект после транзакции — не сохранилось» и «обратился к lazy-полю в контроллере — упало».

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.