Хабрахабр

[Перевод] Как работают методы persist, merge из JPA и методы save, update, saveOrUpdate из Hibernate

Перевод статьи подготовлен специально для студентов курса "Разработчик 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 предоставляет следующие методы перехода состояния сущности.

jpaentitystates

Session в Hibernate реализует все методы EntityManager из JPA и предоставляет несколько дополнительных методов для изменения состояния сущностей, таких как save, saveOrUpdate и update.

hibernateentitystates1

Давайте рассмотрим сущность Book, которая использует Fluent API:

@Entity(name = "Book")
@Table(name = "book")
public class Book 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.

Чтобы изменить состояние сущности с 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
)

Это необходимо поскольку управляемые сущности хранятся в структуре Map, в которой ключ формируется из типа сущности и идентификатора, а значение является ссылкой на сущность. Обратите внимание, что идентификатор (id) присваивается до присоединения сущности Book к текущему Persistence Context. Именно по этой причине JPA EntityManager и Hibernate Session также называются кэшем первого уровня (First-Level Cache).

При вызове метода persist сущность только присоединяется к текущему Persistence Context, и INSERT может быть отложен до вызова flush.

По этой причине Hibernate не может использовать пакетные запросы INSERT для сущностей, использующих IDENTITY-идентификаторы. Единственным исключением является генератор IDENTITY, который запускает INSERT сразу, так как это единственный способ получить идентификатор сущности. в этой статье. Дополнительные сведения о этом см.

Специфичный для Hibernate метод save был в нём еще до появления JPA, с начала проекта Hibernate.

Следовательно, метод save эквивалентен методам update и saveOrUpdate. Метод save инициирует событие SaveOrUpdateEvent, которое обрабатывается обработчиком DefaultSaveOrUpdateEventListener.

Чтобы увидеть как работает метод 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, метод save возвращает идентификатор сущности. Как вы видите, результат идентичен вызову метода persist.

Hibernate-специфичный метод update предназначен для обхода механизма dirty checking (рус.), и принудительного обновления сущности во время flush (сброса).

Следовательно, метод update эквивалентен методам save и saveOrUpdate. Метод update инициирует событие SaveOrUpdateEvent, которое обрабатывается обработчиком DefaultSaveOrUpdateEventListener.

Чтобы увидеть как работает метод 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 для предотвращения не нужных обновлений

Чтобы предотвратить это, вы можете использовать аннотацию Hibernate @SelectBeforeUpdate, которая вызовет SELECT для загрузки сущности, для использования в механизме dirty checking. Теперь UPDATE будет всегда выполняться, даже если сущность не была изменена в то время, когда она была в состоянии Detached.

Итак, если мы аннотируем сущность 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 обнаружил, что сущность не была изменена.

Hibernate-специфичный метод saveOrUpdate — это просто псевдоним для сохранения и обновления.

Следовательно, метод update эквивалентен методам save и saveOrUpdate. Метод saveOrUpdate инициирует событие SaveOrUpdateEvent, которое обрабатывается обработчиком DefaultSaveOrUpdateEventListener.

Теперь вы можете использовать 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)

Чтобы избежать 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.

Ждем всех на дне открытых дверей, где мы подробно расскажем о программе курса и процессе обучения. На этом все.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»