Я уже давно хотел на этот счет написать статью. Она короткая, но имхо она нужна.

Давайте с места в карьер: merge в Hibernate – это не универсальный save, и чаще всего он Вам не нужен.

На самом деле, если абстрагироваться, то это специализированная операция для случая, когда у вас есть detached-сущность, и вы хотите перенести ее состояние в текущий Persistence Context.

Проблема в том, что на практике merge очень часто используют для апдейта любой сущности (Spring Data JPA иногда так делает) и как следствие получают лишние SQL-запросы.

Ниже разберем на мой взгляд главное - почему именно так происходит, где merge действительно нужен, а где он превращается в антипаттерн (P.S да почти везде).

Что делает merge под капотом и почему это важно

Когда Вы или Spring Data JPA вызывает entityManager.merge(entity), Hibernate обычно делает следующее:

  1. Находит или чаще всего загружает managed-копию сущности (через SELECT).

  2. Копирует состояние из detached-объекта в managed-объект.

  3. И уже на 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-сущностью. Например:

  1. Сущность была загружена в одной транзакции/сессии.

  2. Потом оказалась detached (сессия закрыта, объект передан между слоями).

  3. Изменения нужно вернуть в БД в другой транзакции.

Пример:

// Транзакция #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 логичен. Но даже здесь нужно помнить его цену (доп. чтение и копирование состояния).

Вывод и Практические Правила

  • Новая сущность -> persist

  • Managed-сущность -> просто меняем поля, без save/merge

  • Detached-сущность -> merge (если действительно нужен re-attach)

Если держать в голове состояния сущности, 80% проблем вокруг Hibernate исчезают.

На деле merge не то чтобы плохой. Плохая привычка использовать его повсеместно. Было бы справедливо сказать, что Spring Data JPA и её save однозначно добавляют масла в огонь, но там долгая история. Может, у меня дойдут руки и я на этот счет тоже напишу статью.

Удачи!

Комментарии (3)


  1. Zulu0
    07.04.2026 14:25

    У меня возникло ощущение пустоты. Потому что уровень статьи не средний, он начальный. Чуть шире описать проблему. И накидать варианты решения. Например у постгри есть отличный инструмент SQL UPSERT c уникальными ключами. Может стоит покопать в эту сторону?
    Я вот лично JPA обхожу стороной. Но разобраться какраз очень хочется. Именно для наличия кешей и персистенс состояний объектов, чтобы базу лишний раз не дергать.


    1. iamkisly
      07.04.2026 14:25

      Чуть шире описать проблему

      Давайте с места в карьер: merge в Hibernate – это не универсальный save, и чаще всего он Вам не нужен.

      Я думаю если шире описать, то послучится что заголовок должен звучать как "Чаще всего Hibernate вам не нужен"


  1. aleksandy
    07.04.2026 14:25

    Проблема с merge() в спрингдатных репозиториях в абсолютно идиотском решении сделать репозиторий по умолчанию транзакционным. Репозиторий - это только доступ к данным, управление транзакциями - это уже инфраструктурный уровень, и заниматься этим должны соответствующие фасады.

    Причём можно же было сделать по-нормальному, раз уж так хотелось обмазаться аннотациями, и указать @Transactional(propagation = MANDATORY). При таком подходе merge() из save() можно было бы невозбранно выбросить и ничего не поменялось. Но в угоду красивым презентациям сделали как сделали. В результате получаем выходы сущностей за границы транзакций со всеми вытекающими.

    Правильный вариант:

    И тут мы плавно подходим к тому, что декларируемая (ничего общего с реальностью не имеющая, конечно) взаимозаменяемость разных spring-data-XXX идёт лесом, т.к. решили мы заменить JPA на jdbc, и вот уже наше обновление сломалось, просто потому, что save-то никто и не сделал.