Добрый день, друзья. Перевод статьи подготовлен специально для студентов курса "Разработчик Java".
Введение
В этой статье я собираюсь показать вам, как работают методы persist
, merge
из JPA и сравнить их с методами save
, update
, saveOrUpdate
из Hibernate.
Хотя лучше использовать JPA-методы для изменения состояния сущности (рус.),
вы увидите, что метод save
из Hibernate является хорошей альтернативой merge
, когда вы хотите уменьшить количество SQL-запросов, выполняемых во время пакетной обработки (batch processing).
Состояния сущностей
Как описано в этой статье (рус.), сущность в JPA или Hibernate может быть в одном из следующих состояний:
- Transient (New) — Новая
- Managed (Persistent) — Управляемая
- Detached — Отсоединенная
- Removed (Deleted) — Удаленная
Переход из одного состояния в другое осуществляется с помощью методов EntityManager
или Session
.
Например, EntityManager
из JPA предоставляет следующие методы перехода состояния сущности.
Session
в Hibernate реализует все методы EntityManager
из JPA и предоставляет несколько дополнительных методов для изменения состояния сущностей, таких как save
, saveOrUpdate
и update
.
Модель предметной области
Давайте рассмотрим сущность Book
, которая использует Fluent API:
@Entity(name = "Book")
@Table(name = "book")
public class Book {
@Id
@GeneratedValue
private Long id;
private String isbn;
private String title;
private String author;
public Long getId() {
return id;
}
public Book setId(Long id) {
this.id = id;
return this;
}
public String getIsbn() {
return isbn;
}
public Book setIsbn(String isbn) {
this.isbn = isbn;
return this;
}
public String getTitle() {
return title;
}
public Book setTitle(String title) {
this.title = title;
return this;
}
public String getAuthor() {
return author;
}
public Book setAuthor(String author) {
this.author = author;
return this;
}
}
Теперь посмотрим, как мы можем сохранить и обновить сущность с помощью JPA и Hibernate.
Метод persist
Чтобы изменить состояние сущности с Transient (New) на Managed (Persisted), мы можем использовать метод persist
, предлагаемый JPA EntityManager
, который также наследуется в Hibernate Session
.
Методpersist
инициирует событиеPersistEvent
, которое обрабатывается обработчикомDefaultPersistEventListener
.
Поэтому при выполнении следующего примера:
doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
entityManager.persist(book);
LOGGER.info(
"Persisting the Book entity with the id: {}",
book.getId()
);
});
Примечание переводчика: метод doInJPA выполняет операции в JPA-транзакции.
Hibernate генерирует следующие операторы SQL:
CALL NEXT VALUE FOR hibernate_sequence
-- Persisting the Book entity with the id: 1
INSERT INTO book (
author,
isbn,
title,
id
)
VALUES (
'Vlad Mihalcea',
'978-9730228236',
'High-Performance Java Persistence',
1
)
Обратите внимание, что идентификатор (id) присваивается до присоединения сущности Book
к текущему Persistence Context. Это необходимо поскольку управляемые сущности хранятся в структуре Map
, в которой ключ формируется из типа сущности и идентификатора, а значение является ссылкой на сущность. Именно по этой причине JPA EntityManager
и Hibernate Session
также называются кэшем первого уровня (First-Level Cache).
При вызове метода persist
сущность только присоединяется к текущему Persistence Context, и INSERT может быть отложен до вызова flush
.
Единственным исключением является генератор IDENTITY, который запускает INSERT сразу, так как это единственный способ получить идентификатор сущности. По этой причине Hibernate не может использовать пакетные запросы INSERT для сущностей, использующих IDENTITY-идентификаторы. Дополнительные сведения о этом см. в этой статье.
Метод save
Специфичный для Hibernate метод save
был в нём еще до появления JPA, с начала проекта Hibernate.
Методsave
инициирует событиеSaveOrUpdateEvent
, которое обрабатывается обработчикомDefaultSaveOrUpdateEventListener
. Следовательно, методsave
эквивалентен методамupdate
иsaveOrUpdate
.
Чтобы увидеть как работает метод save
, рассмотрим следующий пример:
doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
Session session = entityManager.unwrap(Session.class);
Long id = (Long) session.save(book);
LOGGER.info(
"Saving the Book entity with the id: {}",
id
);
});
При выполнении приведенного выше примера Hibernate генерирует следующий SQL:
CALL NEXT VALUE FOR hibernate_sequence
-- Saving the Book entity with the id: 1
INSERT INTO book (
author,
isbn,
title,
id
)
VALUES (
'Vlad Mihalcea',
'978-9730228236',
'High-Performance Java Persistence',
1
)
Как вы видите, результат идентичен вызову метода persist
. Однако, в отличие от persist
, метод save
возвращает идентификатор сущности.
Метод update
Hibernate-специфичный метод update
предназначен для обхода механизма dirty checking (рус.), и принудительного обновления сущности во время flush (сброса).
Методupdate
инициирует событиеSaveOrUpdateEvent
, которое обрабатывается обработчикомDefaultSaveOrUpdateEventListener
. Следовательно, методupdate
эквивалентен методамsave
иsaveOrUpdate
.
Чтобы увидеть как работает метод update
, рассмотрим пример, в котором в транзакции сохраняется сущность Book
, затем изменяется, пока сущность находится в состоянии Detached, и после этого принудительно вызывается SQL UPDATE, используя метод update
.
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
entityManager.persist(book);
return book;
});
LOGGER.info("Modifying the Book entity");
_book.setTitle(
"High-Performance Java Persistence, 2nd edition"
);
doInJPA(entityManager -> {
Session session = entityManager.unwrap(Session.class);
session.update(_book);
LOGGER.info("Updating the Book entity");
});
При выполнении приведенного выше примера Hibernate генерирует следующие операторы SQL:
CALL NEXT VALUE FOR hibernate_sequence
INSERT INTO book (
author,
isbn,
title,
id
)
VALUES (
'Vlad Mihalcea',
'978-9730228236',
'High-Performance Java Persistence',
1
)
-- Modifying the Book entity
-- Updating the Book entity
UPDATE
book
SET
author = 'Vlad Mihalcea',
isbn = '978-9730228236',
title = 'High-Performance Java Persistence, 2nd edition'
WHERE
id = 1
Обратите внимание, что UPDATE выполняется во время сброса (flush) Persistence Context, прямо перед коммитом, и поэтому сначала логгируется сообщение "Updating the Book entity".
Использование @SelectBeforeUpdate для предотвращения не нужных обновлений
Теперь UPDATE будет всегда выполняться, даже если сущность не была изменена в то время, когда она была в состоянии Detached. Чтобы предотвратить это, вы можете использовать аннотацию Hibernate @SelectBeforeUpdate
, которая вызовет SELECT для загрузки сущности, для использования в механизме dirty checking.
Итак, если мы аннотируем сущность Book
аннотацией @SelectBeforeUpdate
:
@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {
// Код опущен для краткости
}
И выполним следующий пример:
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
entityManager.persist(book);
return book;
});
doInJPA(entityManager -> {
Session session = entityManager.unwrap(Session.class);
session.update(_book);
});
Hibernate выполнит следующие инструкции SQL:
INSERT INTO book (
author,
isbn,
title,
id
)
VALUES (
'Vlad Mihalcea',
'978-9730228236',
'High-Performance Java Persistence',
1
)
SELECT
b.id,
b.author AS author2_0_,
b.isbn AS isbn3_0_,
b.title AS title4_0_
FROM
book b
WHERE
b.id = 1
Обратите внимание, что на этот раз UPDATE не выполняется, поскольку механизм dirty checking обнаружил, что сущность не была изменена.
Метод saveOrUpdate
Hibernate-специфичный метод saveOrUpdate
— это просто псевдоним для сохранения и обновления.
МетодsaveOrUpdate
инициирует событиеSaveOrUpdateEvent
, которое обрабатывается обработчикомDefaultSaveOrUpdateEventListener
. Следовательно, методupdate
эквивалентен методамsave
иsaveOrUpdate
.
Теперь вы можете использовать saveOrUpdate
, когда хотите сохранить сущность или принудительно выполнить UPDATE, как показано в следующем примере.
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
Session session = entityManager.unwrap(Session.class);
session.saveOrUpdate(book);
return book;
});
_book.setTitle("High-Performance Java Persistence, 2nd edition");
doInJPA(entityManager -> {
Session session = entityManager.unwrap(Session.class);
session.saveOrUpdate(_book);
});
Опасайтесь исключений NonUniqueObjectException
Одна из проблем, которая может возникнуть с save
, update
и saveOrUpdate
, заключается в том, что Persistence Context уже содержит ссылку на сущность с тем же идентификатором и того же типа:
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
Session session = entityManager.unwrap(Session.class);
session.saveOrUpdate(book);
return book;
});
_book.setTitle(
"High-Performance Java Persistence, 2nd edition"
);
try {
doInJPA(entityManager -> {
Book book = entityManager.find(
Book.class,
_book.getId()
);
Session session = entityManager.unwrap(Session.class);
session.saveOrUpdate(_book);
});
} catch (NonUniqueObjectException e) {
LOGGER.error(
"The Persistence Context cannot hold " +
"two representations of the same entity",
e
);
}
Теперь Hibernate бросит исключение NonUniqueObjectException
, потому что второй EntityManager
уже содержит объект Book
с тем же идентификатором, что и тот, который мы передали для обновления, и Persistence Context не может содержать два представления одной и той же сущности.
org.hibernate.NonUniqueObjectException:
A different object with the same identifier value was already associated with the session : [com.vladmihalcea.book.hpjp.hibernate.pc.Book#1]
at org.hibernate.engine.internal.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:651)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:227)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:92)
at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73)
at org.hibernate.internal.SessionImpl.fireSaveOrUpdate(SessionImpl.java:682)
at org.hibernate.internal.SessionImpl.saveOrUpdate(SessionImpl.java:674)
Метод merge
Чтобы избежать NonUniqueObjectException
, необходимо использовать метод merge
из JPA EntityManager
, который унаследован также в Hibernate Session
.
Как объяснено в этой статье (рус.), метод merge
извлекает сущность из базы данных, если в Persistence Context не найдена ссылка на эту сущность, и копирует состояние detached-сущности, переданной в метод merge
, в извлечённую сущность.
Методmerge
инициирует событиеMergeEvent
, которое обрабатывается обработчикомDefaultMergeEventListener
.
Чтобы увидеть, как работает метод merge
, рассмотрим пример, в котором сущность Book
сохраняется в транзакции, затем модифицируется, пока она находится в состоянии Detached, после этого detached-сущность передается в merge
.
Book _book = doInJPA(entityManager -> {
Book book = new Book()
.setIsbn("978-9730228236")
.setTitle("High-Performance Java Persistence")
.setAuthor("Vlad Mihalcea");
entityManager.persist(book);
return book;
});
LOGGER.info("Modifying the Book entity");
_book.setTitle(
"High-Performance Java Persistence, 2nd edition"
);
doInJPA(entityManager -> {
Book book = entityManager.merge(_book);
LOGGER.info("Merging the Book entity");
assertFalse(book == _book);
});
При выполнении приведенного выше примера Hibernate выполнил следующие операторы SQL:
INSERT INTO book (
author,
isbn,
title,
id
)
VALUES (
'Vlad Mihalcea',
'978-9730228236',
'High-Performance Java Persistence',
1
)
-- Modifying the Book entity
SELECT
b.id,
b.author AS author2_0_,
b.isbn AS isbn3_0_,
b.title AS title4_0_
FROM
book b
WHERE
b.id = 1
-- Merging the Book entity
UPDATE
book
SET
author = 'Vlad Mihalcea',
isbn = '978-9730228236',
title = 'High-Performance Java Persistence, 2nd edition'
WHERE
id = 1
Обратите внимание, что ссылка на сущность, возвращаемая merge
, отличается от отсоединенной (detached), которую мы передали методу merge
.
Хотя при копировании состояния detached-сущности лучше использовать JPA-метод merge
, дополнительный SELECT может быть проблематичным при выполнении пакетной обработки.
По этой причине лучше использовать update
, когда вы уверены, что в Persistence Context нет ссылки на эту сущность, и что detached-сущность была изменена. Дополнительные сведения по этой теме см. этой статье.
Выводы
Для сохранения сущности следует использовать метод JPA persist
.
Для копирования состояния detached-сущности предпочтительным является merge
.
Метод update
полезен только для задач пакетной обработки.
Методы save
и saveOrUpdate
— это просто псевдонимы для update
, и вам не следует использовать их вообще.
Некоторые разработчики используют save
, даже если объект уже управляется,
но это ошибка и вызывает лишнее событие, так как для управляемых сущностей UPDATE автоматически обрабатывается Persistence context во время flush.
На этом все. Ждем всех на дне открытых дверей, где мы подробно расскажем о программе курса и процессе обучения.
Комментарии (10)
usharik
03.09.2019 17:01А чем плохо использования merge вместо persist для новых сущностей? Насколько я знаю из собственного опыта, это работает.
aleksandy
03.09.2019 21:20ЕМНИП, если идентификатор генерируется Hibernate-ом, то при вызове persist() текущее его значение будет проигнорировано (Hibernate сгенерирует новое), при вызове merge() замены не происходит.
Rusty_Fox
03.09.2019 22:26Соглашусь. Ещё бывают ситуации, когда не знаешь, работаешь с новым экземпляром или обновляет уже существующий.
theco6ak
а разве Jpa — это не спецификация (читай api), а hibernate её ревлизация?
DSolodukhin
Там не всё так просто, первая версия Hibernate появилась за несколько лет до первой версии JPA. Собственно, стандарт JPA писали во многом под впечатлением от Hibernate.
Сам Hibernate предоставляет API, совместимое с JPA (является реализацией стандарта), но при этом у него есть и свое собственное API, не совместимое ни с чем.
Mishiko