В чём разница между persist() и merge()
Методы persist() и merge() интерфейса EntityManager отвечают за перевод сущностей под управление контекста постоянства (PersistenceContext), однако они предназначены для принцип...
Методы persist() и merge() интерфейса EntityManager отвечают за перевод сущностей под управление контекста постоянства (PersistenceContext), однако они предназначены для принципиально разных исходных состояний объектов, имеют разную семантику работы с памятью и генерируют разные последовательности SQL-запросов.
🟢 Junior Level
Главные отличия: краткая сводка
persist(entity): Предназначен для новых сущностей в состоянииTransient(еще не имеющих записи в базе данных). Делает переданный Java-объект управляемым (Managed) и ничего не возвращает (void).merge(entity): Предназначен для сущностей в состоянииDetached(ранее сохраненных, но отвязанных от текущей сессии). Находит или загружает копию из БД, переносит в нее поля из переданного объекта и возвращает ссылку на новый управляемый экземпляр.
PERSIST VS MERGE: ПОВЕДЕНИЕ В ПАМЯТИ
1. ВЫЗОВ PERSIST:
[ user (Transient) ] ──persist(user)──> [ user (Managed) ]
(Тот же самый экземпляр становится управляемым, возвращает void)
2. ВЫЗОВ MERGE:
[ detachedUser ] ──merge(detachedUser)──> [ managedUser (Managed) ]
│
(detachedUser ОСТАЕТСЯ Detached! ▼
Все дальнейшие операции нужно вести строго с managedUser!)
Сравнительная таблица
| Критерий | entityManager.persist(entity) |
entityManager.merge(entity) |
|---|---|---|
| Целевое состояние | Transient (новый объект) |
Detached (отсоединенный объект) |
| Возвращаемое значение | void |
Ссылка на новый Managed экземпляр |
| Статус переданного объекта | Становится Managed |
Остается Detached |
| Генерируемый SQL | Только INSERT (при flush) |
Сначала SELECT, затем UPDATE или INSERT |
| Поведение при наличии ID в БД | Бросает EntityExistsException |
Обновляет существующую строку |
Базовые примеры
// Использование persist():
User newUser = new User();
newUser.setName("Максим");
entityManager.persist(newUser);
// newUser теперь Managed. Можно менять поля:
newUser.setName("Максим Обновленный"); // Dirty checking сохранит это при flush
// Использование merge():
User detachedUser = getFromCacheOrRest(); // Объект с ID=1, но вне сессии
detachedUser.setName("Новое имя");
// ОШИБКА:
// entityManager.merge(detachedUser);
// detachedUser.setEmail("test@ya.ru"); // НЕ сохранится!
// ПРАВИЛЬНО:
User managedUser = entityManager.merge(detachedUser);
managedUser.setEmail("test@ya.ru"); // Сохранится в БД!
🟡 Middle Level
Внутренний алгоритм работы merge()
Когда приложение вызывает entityManager.merge(detachedEntity):
- Hibernate извлекает первичный ключ (
id) из переданного объекта. - Проверяет наличие сущности с таким
idв кэше первого уровня L1 (entitiesByKey). - Если сущности в сессии нет — выполняет SQL-запрос
SELECTв базу данных. - Если запись в БД найдена:
- Создает/берет
Managedэкземпляр. - Копирует все поля из
detachedEntityв этотManagedэкземпляр. - Планирует операцию SQL
UPDATEвActionQueue(если Dirty Checking найдет отличия).
- Создает/берет
- Если запись в БД не найдена (или ID был
null):- Создает новый
Managedэкземпляр. - Копирует поля.
- Планирует операцию SQL
INSERT.
- Создает новый
- Возвращает ссылку на полученный
Managedобъект.
Почему merge() для новых сущностей — антипаттерн
Часто разработчики вызывают merge() для сохранения всех сущностей подряд, не задумываясь об их состоянии:
User newUser = new User();
newUser.setName("Олег");
entityManager.merge(newUser); // Формально сработает, НО...
Цена ошибки:
- Если генератор ID не определен или используется ручное присвоение ID, Hibernate выполнит лишний штрафной
SELECT, чтобы убедиться, что записи с таким ID нет в таблице. - Создается лишний дублирующий экземпляр объекта в оперативной памяти JVM.
- Для новых сущностей всегда следует использовать
persist().
Каскадные операции: CascadeType.PERSIST vs CascadeType.MERGE
Если родительская сущность содержит дочерние коллекции:
@OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST)
private List<OrderItem> items = new ArrayList<>();
- Если вызвать
entityManager.persist(order), каскад передастся на все элементыitems. - Если же для
orderвызватьentityManager.merge(order), но в аннотации отсутствуетCascadeType.MERGE, связанные дочерние элементы не будут объединены с сессией и их изменения потеряются.
🔴 Senior Level
Реализация в Spring Data JPA: Как работает repository.save()
Многие разработчики уверены, что Spring Data JPA CrudRepository.save() вызывает какой-то специальный магический метод. В действительности под капотом SimpleJpaRepository реализован простой выбор:
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
Проблема «Assigned ID» (Идентификаторов, задаваемых вручную):
Как Spring Data определяет entityInformation.isNew(entity):
- По умолчанию: если поле
@Idобъекта равноnull(или0для примитивов), объект считается новым (isNew = true), вызываетсяpersist(). - Если ID задается вручную (например, бизнес-ключ
String uuid,taxNumberили композитный ключ):- Поле ID уже заполнено до сохранения (
id != null). - Spring ошибочно считает сущность
Detached(isNew = false)! - Вместо прямого
INSERTSpring вызываетentityManager.merge(entity). - Hibernate выполняет паразитный предварительный SQL
SELECT, получаетnullи только затем делаетINSERT.
- Поле ID уже заполнено до сохранения (
Архитектурное решение:
Сущность должна реализовать интерфейс org.springframework.data.domain.Persistable<ID>:
@Entity
public class Payment implements Persistable<String> {
@Id
private String transactionUuid;
@Transient
private boolean isNewEntity = true;
@Override
public String getId() { return transactionUuid; }
@Override
public boolean isNew() { return isNewEntity; }
@PostPersist
@PostLoad
void markNotNew() { this.isNewEntity = false; }
}
Теперь Spring вызовет быстрый persist() без лишнего SELECT.
Опасность каскадного merge() на непроинициализированных Lazy коллекциях
Представим сценарий:
- В первой транзакции загружен
Departmentс ленивой коллекциейemployees(FetchType.LAZY). - Коллекция
employeesне была тронута (осталась неинициализированным проксиPersistentBag). - Сессия закрылась, объект стал
Detached. - Во второй транзакции вызывается:
Department managedDept = entityManager.merge(detachedDept); - Внутреннее поведение:
DefaultMergeEventListenerв Hibernate проверяет состояние коллекций. Если коллекцияemployeesпомеченаCascadeType.MERGEилиALL, и она не была инициализирована:- Hibernate оставляет существующую коллекцию в
managedDeptнепроинициализированной. - Однако, если коллекция была заменена в detached-объекте на обычный
new ArrayList<>(), Hibernate посчитает, что старые сотрудники были удалены (Orphan Removal), и выполнит их удаление из БД!
- Hibernate оставляет существующую коллекцию в
4 Tricky Questions
1. Что произойдет, если в текущей сессии уже загружен User(id=1) (Managed), а мы вызываем entityManager.persist(user2) с тем же id=1? А если вызвать entityManager.merge(user2)?
Ответ:
- Для
persist(user2): Hibernate проверяет картуentitiesByKey. Обнаружив, что сущность с таким типом и ID уже находится вPersistenceContext, он немедленно выбросит исключение:jakarta.persistence.EntityExistsException(илиNonUniqueObjectException). - Для
merge(user2): Никакой ошибки не будет. Hibernate возьмет уже находящийся в сессии экземплярUser(id=1), скопирует в него поля изuser2и вернет ссылку на существующийManagedобъект. Никакого нового SQLSELECTвыполняться не будет, так как объект уже есть в L1 кэше.
2. Почему сигнатура persist() возвращает void, а merge() возвращает объект T?
Ответ:
persist(e)мутирует статус переданного Java-объекта прямо по месту (In-place mutation). Объектe, на который у вас уже есть ссылка, сам становится управляемым (Managed) вPersistenceContext. Возвращать его бессмысленно.merge(e)не может сделать переданный объектeуправляемым, потому что вPersistenceContextуже может находиться другой экземпляр для этой же строки БД (нарушилась бы Identity Guarantee). Поэтомуmerge()вынужден вернуть ссылку на тот экземпляр, который реально привязан к сессии, оставляя исходныйeв статусеDetached.
3. Мы передали объект в состоянии Removed в метод persist(), и в метод merge(). Каковы будут результаты?
Ответ:
persist(removedEntity): Спецификация JPA прямо разрешает отменить удаление. Статус сущности возвращается вManaged, а запланированный вActionQueueзапрос SQLDELETEудаляется.merge(removedEntity): Спецификация JPA запрещает слияние удаленной сущности. ВызовentityManager.merge(removedEntity)выбросит исключениеjava.lang.IllegalArgumentException.
4. Что произойдет, если передать Transient объект с незаполненным ID в merge(), если у сущности стратегия ID — GenerationType.SEQUENCE?
Ответ:
- Метод
merge()распознает объект как новый (так какid == null). - Hibernate обратится к базе данных за следующим значением последовательности (
SELECT nextval(...)). - Создаст новый экземпляр сущности, присвоит ему сгенерированный ID, скопирует остальные поля и переведет новый объект в статус
Managed. - Исходный объект останется со значением
id == nullи в статусеTransient. - Метод вернет новый управляемый объект с уже присвоенным ID.
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«Метод
persist()переводит новую (Transient) сущность в состояниеManaged. Он возвращаетvoid, переданный объект становится управляемым, а операция планирует только SQLINSERTбез лишних чтений. Методmerge()переводит отсоединенную (Detached) сущность вManaged. Он выполняетSELECT, копирует поля во вновь найденный или созданный объект и возвращает ссылку на него, оставляя исходный экземплярDetached. В Spring Data JPA методsave()внутри себя проверяетisNew(): для новых объектов вызываетpersist(), для существующих —merge(). При ручном задании ID сущности должны реализовыватьPersistable<ID>, чтобы избежать лишнегоSELECTперед вставкой».
Ключевые метрики для Production-мониторинга
- Количество SELECT перед INSERT при операциях сохранения: сигнализирует о неэффективном использовании
merge()для новых сущностей с назначенными ключами. - Длительность выполнения
repository.save(): аномальный рост указывает на каскадную проверку больших графов объектов внутриmerge().
Красные флаги (Чего говорить НЕЛЬЗЯ)
- ❌ «Метод merge() делает исходный переданный объект управляемым» — Грубая ошибка. Исходный объект остается detached, возвращается новая копия.
- ❌ «Всегда использую merge(), потому что он универсальный и сам решает, делать INSERT или UPDATE» — Это порождает лишний предварительный
SELECTи двойной оверхед по памяти. - ❌ «После вызова entityManager.persist() сразу выполняется SQL INSERT» — Только при стратегии
IDENTITY. Во всех остальных случаяхINSERTоткладывается до фазыflush(). - ❌ «persist() можно безопасно вызывать для detached-объекта» — Бросит
EntityExistsExceptionпри наличии ID в сессии или базе.