Как работают persist и merge в JPA

Введение

При использовании 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 автоматически синхронизирует состояние сущности со строкой в БД.

(Visited 7 406 times, 16 visits today)

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

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.