Я уже давно хотел на этот счет написать статью. Она короткая, но имхо она нужна.
Давайте с места в карьер: merge в Hibernate – это не универсальный save, и чаще всего он Вам не нужен.
На самом деле, если абстрагироваться, то это специализированная операция для случая, когда у вас есть detached-сущность, и вы хотите перенести ее состояние в текущий Persistence Context.
Проблема в том, что на практике merge очень часто используют для апдейта любой сущности (Spring Data JPA иногда так делает) и как следствие получают лишние SQL-запросы.
Ниже разберем на мой взгляд главное - почему именно так происходит, где merge действительно нужен, а где он превращается в антипаттерн (P.S да почти везде).
Что делает merge под капотом и почему это важно
Когда Вы или Spring Data JPA вызывает entityManager.merge(entity), Hibernate обычно делает следующее:
Находит или чаще всего загружает managed-копию сущности (через
SELECT).Копирует состояние из detached-объекта в managed-объект.
И уже на
flushзапускает dirty checking и решает, нужен лиUPDATE.
Ключевая мысль: merge это (внезапно) мердж состояния объектов, а не просто сохранение изменений.
Если вы не в detached-сценарии, то эта механика почти всегда избыточна.
P.S: Я быстро напомню, что у нас идёт набор на программу по Hibernate для разработчиков в рамках Spring АйО Академии. Там этот и более сложные enterprise кейсы будут разбираться детальнее. Давайте теперь дальше.
Почему merge часто вреден
1. Лишний SELECT перед UPDATE
Для detached-сущностей merge нередко делает дополнительный SELECT, чтобы получить актуальное managed-состояние.
Это означает дополнительный round-trip в БД. На единичном запросе понятное дело не страшно, но в потоке запросов и особенно в batch-овой обработке становится ощутимо.
2. Ненужная работа, если сущность уже managed
Если сущность уже находится в текущем Persistence Context, Hibernate и так отследит изменения через dirty checking. Вызывать merge (или save, который под капотом может вызвать merge) в таком случае не нужно.
3. Каскадирование может сильно усугубить проблему
Если на связях стоит CascadeType.MERGE (а мы знаем, что иногда вообще делают CascadeType.ALL, что часто довольно плохая идея), то операция распространяется по графу сущностей. Чем больше граф, тем выше накладные расходы: больше обработки, больше копирований, потенциально больше SQL.
Антипаттерн “find -> change -> save”
Очень типичный сервисный метод, ну просто база (я из-за него и пишу статью). Вот давайте честно, я знаю, что у подавляющего числа из Вас есть этот код в продакшене:
@Transactional public void renamePost(Long id, String newTitle) { Post post = postRepository.findById(id).orElseThrow(); post.setTitle(newTitle); // Антипаттерн: в Spring Data JPA это часто уходит в merge postRepository.save(post); }
Почему это плохо:
Очевидно, что
postуже managed (мы его загрузили в этой же транзакции);Hibernate сам выполнит
UPDATEна flush/commit (надежда на dirty check);saveздесь ничего полезного не добавляет, но может добавлять накладные расходы.
Правильный вариант:
@Transactional public void renamePost(Long id, String newTitle) { Post post = postRepository.findById(id).orElseThrow(); // Всё. Больше ничего не нужно post.setTitle(newTitle); }
Когда merge Действительно Уместен?
Ну не просто же так торпеду оставили в коде? Конечно же нет.
Сам по себе merge имеет место когда вы реально работаете с detached-сущностью. Например:
Сущность была загружена в одной транзакции/сессии.
Потом оказалась detached (сессия закрыта, объект передан между слоями).
Изменения нужно вернуть в БД в другой транзакции.
Пример:
// Транзакция #1 Post detached = txTemplate.execute(status -> entityManager.find(Post.class, 1L) ); // Сущность detached detached.setTitle("New title"); // Транзакция #2 txTemplate.execute(status -> { entityManager.merge(detached); return null; });
Пример конечно немного искусственный, но мысль понятна. Здесь merge логичен. Но даже здесь нужно помнить его цену (доп. чтение и копирование состояния).
Вывод и Практические Правила
Новая сущность ->
persistManaged-сущность -> просто меняем поля, без
save/mergeDetached-сущность ->
merge(если действительно нужен re-attach)
Если держать в голове состояния сущности, 80% проблем вокруг Hibernate исчезают.
На деле merge не то чтобы плохой. Плохая привычка использовать его повсеместно. Было бы справедливо сказать, что Spring Data JPA и её save однозначно добавляют масла в огонь, но там долгая история. Может, у меня дойдут руки и я на этот счет тоже напишу статью.
Удачи!
