Рубрики
Без рубрики

Как сохранить и объединить работу в JPA

Узнайте, как работают операции сохранения и слияния сущностей при использовании JPA и гибернации, а также при очистке контекста сохранения.

Автор оригинала: Vlad Mihalcea.

Вступление

В этой статье я собираюсь объяснить, как работают операции сохранения и слияния сущностей при использовании JPA и гибернации.

При использовании JPA переходы состояния сущности автоматически преобразуются в операторы SQL. В этом посте будет объяснено, когда использовать сохраняться и когда использовать слияние .

Настаивать

Операция 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 к текущему контексту сохранения. Оператор INSERT SQL может быть выполнен непосредственно или отложен до времени сброса .

ИДЕНТИЧНОСТЬ

Если сущность использует генератор идентификаторов:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

ВСТАВКА выполняется сразу же, и Hibernate генерирует следующий вывод:

INSERT INTO post (id, title) 
VALUES (DEFAULT, 'High-Performance Java Persistence')

-- The Post entity identifier is 1

-- Flush Persistence Context

Всякий раз, когда сущность сохраняется, Hibernate должен присоединять ее к текущему контексту сохранения, который действует как Карта сущностей. Ключ Map формируется из типа сущности (ее Java Класса ) и идентификатора сущности.

Для столбцов IDENTITY единственный способ узнать значение идентификатора-выполнить ВСТАВКУ SQL. Следовательно, ВСТАВКА выполняется при вызове метода persist и не может быть отключена до времени сброса.

По этой причине Hibernate отключает пакетные вставки JDBC для сущностей, использующих стратегию ИДЕНТИФИКАЦИЯ генератор.

ПОСЛЕДОВАТЕЛЬНОСТЬ

При использовании стратегии ПОСЛЕДОВАТЕЛЬНОСТИ идентификатора и повторном запуске того же примера 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 может быть отложен до времени сброса, и Hibernate может применить оптимизацию пакетной вставки, если вы зададите свойство конфигурации размера пакета.

Стратегия ТАБЛИЦА ведет себя как ПОСЛЕДОВАТЕЛЬНОСТЬ , но вам следует избегать этого любой ценой, поскольку для создания идентификатора сущности используется отдельная транзакция, что оказывает давление на базовый пул соединений и журнал транзакций базы данных.

Что еще хуже, блокировки на уровне строк используются для координации нескольких одновременных запросов, и, как говорит нам Закон Амдала , введение сериализуемого выполнения может повлиять на масштабируемость.

Для получения более подробной информации о том, почему вам следует избегать ТАБЛИЦЫ стратегии, ознакомьтесь с этой статьей .

Поглощать

Слияние требуется только для отдельных объектов.

Предполагая, что у нас есть следующая сущность:

Post post = doInJPA(entityManager -> {
    Post _post = new Post();
    _post.setTitle("High-Performance Java Persistence");

    entityManager.persist(_post);
    return _post;
});

Поскольку EntityManager , который загрузил объект Post , был закрыт, Post отсоединяется, и режим гибернации больше не может отслеживать какие-либо изменения. Отделенный объект может быть изменен, и для распространения этих изменений объект необходимо повторно подключить к новому контексту сохранения:

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

В то время как для ИДЕНТИЧНОСТИ и ПОСЛЕДОВАТЕЛЬНОСТЬ стратегии генератора, вы можете практически использовать слияние для сохранения сущности для назначенного генератора это было бы менее эффективно.

Учитывая, что Для записи сущности требуется, чтобы идентификаторы назначались вручную:

@Id
private Long id;

При использовании слияние вместо сохранение :

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 (например, java.lang. Long ), для которого Hibernate может проверять возможность обнуления, вместо примитива (например, long) для свойства @Version .

Причина, по которой я хотел показать вам этот пример, заключается в том, что вы можете случайно использовать метод save , подобный этому, предложенному Spring Data SimpleJpaRepository :

@Transactional
public  S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Те же правила применяются и к методу Spring Data save . Если вы когда-либо используете генератор назначенных идентификаторов, вы должны не забыть добавить свойство Java-оболочки @Version , в противном случае будет сгенерирован избыточный оператор SELECT .

Избыточный анти-шаблон сохранения

К настоящему времени ясно, что новые сущности должны проходить через сохраняться , в то время как отдельные сущности должны быть повторно присоединены с помощью слияния . Однако, просматривая множество проектов, я пришел к выводу, что следующий анти-шаблон довольно распространен:

@Transactional
public void savePostTitle(Long postId, String title) {
    Post post = postRepository.findOne(postId);
    post.setTitle(title);
    postRepository.save(post);
}

Метод сохранить не служит никакой цели. Даже если мы удалим его, Hibernate все равно выдаст инструкцию UPDATE , поскольку объект управляется и любое изменение состояния распространяется до тех пор, пока в данный момент выполняется EntityManager открыт.

Это анти-шаблон, потому что сохранить вызов запускает Событие слияния , которое обрабатывается DefaultMergeEventListener , который выполняет следующие операции:

protected void entityIsPersistent(MergeEvent event, Map copyCache) {
    LOG.trace( "Ignoring persistent instance" );

    final Object entity = event.getEntity();
    final EventSource source = event.getSession();
    final EntityPersister persister = source
        .getEntityPersister( event.getEntityName(), entity );

    ( (MergeContext) copyCache ).put( entity, entity, true );

    cascadeOnMerge( source, persister, entity, copyCache );
    copyValues( persister, entity, entity, source, copyCache );

    event.setResult( entity );
}

В вызове метода копировать значения снова копируется гидратированное состояние , поэтому создается новый массив с избыточностью, что приводит к потере циклов процессора. Если сущность имеет дочерние ассоциации и операция слияния также каскадируется от родительских к дочерним сущностям , накладные расходы еще больше, поскольку каждая дочерняя сущность будет распространять Событие слияния и цикл продолжается.

Вывод

Хотя метод save может быть удобен в некоторых ситуациях, на практике никогда не следует вызывать merge для объектов, которые являются новыми или уже управляемыми. Как правило, вы не должны использовать save с JPA. Для новых сущностей вы всегда должны использовать persist , в то время как для отдельных сущностей вам нужно вызвать merge . Для управляемых сущностей вам не нужен метод save , поскольку Hibernate автоматически синхронизирует состояние сущности с базовой записью базы данных.