Состояния entity в Hibernate
Загрузил автора, присвоил новое имя полю и закрыл транзакцию. Никакого сохранения я не вызывал. В следующей транзакции имя в базе оказалось новым.
Это не магия, а поведение по умолчанию: пока объект связан с сессией, библиотека отображения объектов на таблицы (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, остальные разбираются в тренажёре.
- Что такое контекст персистентности (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 нельзя отключить.
- Как Hibernate узнаёт, что managed-сущность изменилась, без явного вызова save?A)Он не узнаёт: чтобы сохранить изменения managed-сущности, нужно явно вызвать save/updateB)Он перехватывает каждое присваивание поля через рефлексию и сразу шлёт UPDATE в базу немедленноC)Через dirty checking: сравнивает состояние с снимком и генерирует UPDATE при flushD)Он требует, чтобы каждое изменяемое поле было помечено аннотацией @Dirty для отслеживания вручную
показать ответ и разбор
+C)Через dirty checking: сравнивает состояние с снимком и генерирует UPDATE при flush// разбор: Пока сущность MANAGED, Hibernate хранит снимок её исходного состояния. При flush (перед запросом/коммитом) он сравнивает текущее состояние со снимком (dirty checking) и генерирует UPDATE ТОЛЬКО для реально изменившихся сущностей/полей — явный save вызывать не нужно (это удивляет новичков: «я не сохранял, а в БД изменилось»). Обратная сторона: случайное изменение managed-сущности молча уйдёт в БД. Поэтому важны границы транзакции, readOnly для запросов (отключает dirty checking) и работа с DTO там, где менять не нужно.
- Чем persist отличается от merge?A)Persist и merge делают одно и то же; merge оставлен лишь для совместимости со старым кодом JPAB)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 бросит исключение.
- Что делает 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.
- В каком состоянии окажется сущность после закрытия сессии/транзакции?A)Removed — закрытие сессии автоматически удаляет из базы все загруженные в ней сущностиB)Detached — она больше не отслеживается контекстомC)Transient — сущность теряет свой id и становится как только что созданная через newD)Persistent — сущность продолжает отслеживаться и после закрытия сессии, до конца работы приложения
показать ответ и разбор
+B)Detached — она больше не отслеживается контекстом// разбор: Когда сессия/EntityManager закрывается (или контекст очищается clear/detach), все managed-сущности переходят в DETACHED: у них сохраняются данные и id, но контекст их больше НЕ отслеживает — изменения полей уже НЕ уходят в БД сами, а обращение к ЛЕНИВЫМ, не загруженным ранее связям бросает LazyInitializationException. Чтобы снова сохранять изменения detached-объекта, его возвращают в контекст через merge. Это ядро частых багов: «поменял объект после транзакции — не сохранилось» и «обратился к lazy-полю в контроллере — упало».
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.