Введение
При использовании JPA переходы между состояниями транслируются автоматически в SQL выражения. В этом посте я попытаюсь объяснить когда использовать persist
и когда использовать merge
.
Persist
Операция persist
должна использоваться только для новых сущностей. В терминах JPA сущность является новой если она была еще связана с строкой в базе данных, это означает что не существует записи в БД соответствующей этой сущности.
Например, при запуске следующего тестового кода:
Post post = new Post(); post.setTitle("High-Performance Java Persistence"); entityManager.persist(post); LOGGER.info("The post entity identifier is {}", post.getId()); LOGGER.info("Flush Persistence Context"); entityManager.flush();
Hibernate присоединит эту сущность Post
к текущему Persistence Context.
SQL выражение INSERT
может быть выполнено либо сразу, либо отложено до flush-time.
IDENTITY
Если сущность использует IDENTITY генератор:
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
INSERT
выполнится сразу и Hibernate сгенерирует следующий запрос:
INSERT INTO post (id, title) VALUES (DEFAULT, 'High-Performance Java Persistence') -- The Post entity identifier is 1 -- Flush Persistence Context
Всякий раз как сущность сохраняется, Hibernate должен прикрепить ее к текущему Persistence Context, который действует как Map сущностей. Ключ Map формируется из типа данных сущности (его класса) и идентификатора сущности.
При использовании IDENTITY
Hibernate не может откладывать выполнение INSERT
до flush-time потому что значение идентификатора может быть сгенерировано только лишь при выполнении выражения. По этой причине Hibernate делает недоступной операцию пакетной вставки JDBC для сущностей использующих стратегию IDENTITY
.
SEQUENCE
При использовании стратегии идентификации SEQUENCE
и перезапуска того же примера, Hibernate сгенерирует следующее:
CALL NEXT VALUE FOR 'hibernate_sequence' -- The post entity identifier is 1 -- Flush Persistence Context INSERT INTO post (title, id) VALUES ('High-Performance Java Persistence', 1)
В этомс случае INSERT
выражение может быть отложено до flush-time, и Hibernate может применить оптимизации пакетной вставки если было установлено свойство размера пакета в конфигурации.
Стратегия TABLE работает так же как SEQUENCE, но нужно избегать ее любой ценой потому что она использует отдельную транзакцию для генерирования идентификатора сущности, таким образом загружая пул соединений и лог транзакций БД,
Merge
Слияние нужно только для detached сущностей.
Предположим у нас есть следующая сущность:
Post post = doInJPA(entityManager -> { Post _post = new Post(); _post.setTitle("High-Performance Java Persistence"); entityManager.persist(_post); return _post; });
Из-за того что EntityManager,
который загрузил сущность Post
уже закрыт, сущность Post
становится detached, и Hibernate больше не может отслеживать изменения. Detached сущность может быть изменена и для распространения этих изменений эта сущность должны быть повторно присоединена к новому Persistence Context.
post.setTitle("High-Performance Java Persistence Rocks!"); doInJPA(entityManager -> { LOGGER.info("Merging the Post entity"); Post post_ = entityManager.merge(post); });
При запуске тестового кода сверху Hibernate выполнит следующие выражения:
-- Merging the Post entity SELECT p.id AS id1_0_0_ , p.title AS title2_0_0_ FROM post p WHERE p.id = 1 UPDATE post SET title='High-Performance Java Persistence Rocks!' WHERE id=1
Hibernate сгенерировал SELECT
выражение первым для извлечения последнего состояния записи в БД, и затем копирует состояние detached сущности в заново извлеченную обслуживаему сущность. Таким образом механизм dirty-checking может обнаружить любое изменения состояние и распространить его в БД,
В то время как для стратегий IDENTITY
и SEQUENCE
можно использовать операцию merge
для сохранения сущности, для назначенных генераторов это решение может быть менее эффективным.
Учитывая, что сущности Post
проставлен вручную идентификатор:
@Id private Long id;
При использовании merge
вместо persist:
doInJPA(entityManager -> { Post post = new Post(); post.setId(1L); post.setTitle("High-Performance Java Persistence"); entityManager.merge(post); });
Hibernate выполнит SELECT выражение для того чтобы убедиться что в БД не существует записи с таким же идентификатором:
SELECT p.id AS id1_0_0_ , p.title AS title2_0_0_ FROM post p WHERE p.id = 1 INSERT INTO post (title, id) VALUES ('High-Performance Java Persistence', 1)
Вы можете решить эту проблему добавив свойство версии к Вашей сущности, таким образом так же предотвратив потерю обновлений в транзакциях с большим количеством запросов:
@Version private Long version;
Важно использовать именно классы-обертки (например java.lang.Long) для того что бы Hibernate мог проверить на возможность появления null, вместо примитивов (например long) для свойства @Version.
Причина по которой я хотел показать этот пример такова, что может быть Вы будете использовать метод save,
который предлагается классом Spring Data SimpleJpaRepository:
@Transactional public <S extends T> S save(S entity) { if (entityInformation.isNew(entity)) { em.persist(entity); return entity; } else { return em.merge(entity); } }
Такие же правила применимы к методу Spring Data save.
Если Вы используете генератор назначающий идентификатор, то должны не забыть добавить свойство @Version с классом-оберткой, чтобы избежать генерирования лишнего SELECT
выражения.
Анти-паттерн метода save
К этому моменту должно ясно пониматься, что новые сущности должны сохранятся через persist
, а detached
сущности должны быть повторно прикреплены используя merge
. Однако, просматривая множество проектов, я пришел к выводу что следующий анти-паттерн достаточно распространен:
@Transactional public void savePostTitle(Long postId, String title) { Post post = postRepository.findOne(postId); post.setTitle(title); postRepository.save(post); }
Метод save
используется не по назначению. Даже если мы удалим его, Hibernate еще будет выполнять выражение UPDATE
, так как сущность управляема и любое изменение состояние будет распространено в БД плка текущий EntityManager
открыт.
Это является анти-паттерном потому что вызов метода save
вызовет MergeEvent
которое управляется DefaultMergeEventListener
который выполняет следующий код:
<code>protected void entityIsPersistent(MergeEvent event, Map copyCache) {</code> <code> LOG.trace( "Ignoring persistent instance" );</code> <code> final Object entity = event.getEntity();</code> <code> final EventSource source = event.getSession();</code> <code> final EntityPersister persister = source</code> <code> .getEntityPersister( event.getEntityName(), entity );</code> <code> ( (MergeContext) copyCache ).put( entity, entity, true );</code> <code> cascadeOnMerge( source, persister, entity, copyCache );</code> <code> copyValues( persister, entity, entity, source, copyCache );</code> <code> event.setResult( entity );</code> <code>}</code>
В вызове метода copyValues,
состояние скопируется снова, и создастся лишний новый массив, на который потратятся ресурсы. Если сущность имеет дочерние связи и операция merge
также каскадно выполнится на дочерних сущностях, расходы будут еще больше потому что каждая дочерняя сущность также вызовет MergeEvent и цикл продолжится.
Заключение
В то время как метод save
может быть удобен для некоторых ситуаций, на практике, вы не должны вызывать merge
для новых или уже контролируемых сущностей. Как показывает практика, Вы не должны использовать метод save
с JPA. Для новых сущностей, всегда нужно использовать persist,
а для detached
сущностей нужно вызывать merge
. Для контролируемых сущностей не нужно вызывать метод save,
потому что Hibernate автоматически синхронизирует состояние сущности со строкой в БД.