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

Как работает свойство версии сущности при использовании JPA и гибернации

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

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

В этой статье я собираюсь показать вам, как свойство JPA @Version entity работает при использовании режима гибернации.

Наиболее существенным преимуществом добавления свойства версии в объект JPA является то , что мы можем предотвратить аномалию потерянного обновления, тем самым гарантируя, что целостность данных не будет нарушена.

Давайте рассмотрим, что у нас есть следующий Продукт объект в нашем приложении:

@Entity(name = "Product")
@Table(name = "product")
public class Product {

    @Id
    private Long id;

    private int quantity;

    @Version
    private int version;

    //Getters and setters omitted for brevity
}

Обратите внимание, что свойство version использует аннотацию JPA @Version , которая указывает Hibernate, что это свойство будет использоваться для оптимистичного механизма блокировки.

При сохранении Продукта сущности:

Product product = new Product();
product.setId(1L);

entityManager.persist(product);

Hibernate будет использовать начальное значение версии 0 который автоматически присваивается JVM, поскольку свойство version является примитивным целочисленным значением.

INSERT INTO product (
    quantity, 
    version, 
    id
) 
VALUES (
    0, 
    0, 
    1
)

При извлечении и изменении объекта Product :

Product product = entityManager.find(
    Product.class, 
    1L
);

product.setQuantity(5);

Hibernate использует свойство version в предложении WHERE выполняемой инструкции UPDATE:

UPDATE 
    product 
SET 
    quantity = 5, 
    version = 1 
WHERE 
    id = 1 AND 
    version = 0

Все инструкции INSERT, UPDATE и DELETE, выполняемые Hibernate, выполняются с помощью метода executeUpdate объекта JDBC PreparedStatement .

Метод executeUpdate возвращает целое число, представляющее количество записей, на которые влияют операторы DML. В нашем случае мы ожидаем, что значение 1 поскольку существует только один Продукт объект, имеющий указанный идентификатор. Более того, включив свойство version , мы проверяем, не изменилась ли ранее загруженная нами сущность между операциями чтения и записи.

Итак, если возвращаемое значение не 1 , затем возникает исключение Устаревшего состояния , которое будет завернуто в исключение JPA OptimisticLockException при загрузке в спящий режим с использованием JPA.

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

При удалении версионного объекта:

Product product = entityManager.getReference(
    Product.class, 
    1L
);

entityManager.remove(product);

Hibernate будет использовать свойство version в предложении WHERE соответствующего оператора DELETE :

DELETE FROM 
    product 
WHERE 
    id = 1 AND 
    version = 1

Чтобы понять, как свойство версии может помочь предотвратить потерю обновлений, рассмотрим следующий пример:

Этот пример можно резюмировать следующим образом:

  1. Алиса загружает Продукт объект, который имеет количественное значение 5 и версия из 1 .
  2. Задание пакетного процессора обновляет Количество продукта до 0 и версия теперь 2 .
  3. Алиса пытается купить новый Товар , следовательно, количество Товара уменьшается.
  4. Когда EntityManager Алисы сброшен, ОБНОВЛЕНИЕ будет выполнено с использованием старого версии значения, следовательно, OptimisticLockException будет выдано, потому что Продукт версия изменилась.

Этот пример инкапсулирован в следующем тестовом примере:

Product product = entityManager.find(Product.class, 1L);

executeSync(() -> doInJPA(_entityManager -> {
    LOGGER.info("Batch processor updates product stock");
    
    Product _product = _entityManager.find(
        Product.class, 
        1L
    );
    _product.setQuantity(0);
}));

LOGGER.info("Changing the previously loaded Product entity");
product.setQuantity(4);

При выполнении приведенного выше тестового случая Hibernate выполняет следующие инструкции SQL:

DEBUG [Alice]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    SELECT 
        p.id as id1_0_0_, 
        p.quantity as quantity2_0_0_, 
        p.version as version3_0_0_ 
    FROM 
        product p 
    WHERE 
        p.id = ?
"], 
Params:[(
    1
)]

-- Batch processor updates product stock

DEBUG [Bob]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    SELECT 
        p.id as id1_0_0_, 
        p.quantity as quantity2_0_0_, 
        p.version as version3_0_0_ 
    FROM 
        product p 
    WHERE 
        p.id = ?
"], 
Params:[(
    1
)]

DEBUG [Bob]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    UPDATE 
        product 
    SET 
        quantity = ?, 
        version = ? 
    WHERE 
        id=? AND 
        version=?
"], 
Params:[(
    0, 
    2, 
    1, 
    1
)]

-- Changing the previously loaded Product entity

DEBUG [Alice]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    UPDATE 
        product 
    SET 
        quantity = ?, 
        version = ? 
    WHERE 
        id=? AND 
        version=?
"], 
Params:[(
    4, 
    2, 
    1, 
    1
)]

ERROR [Alice]: o.h.i.ExceptionMapperStandardImpl - HHH000346: 
Error during managed flush [Row was updated or deleted by another transaction 
(or unsaved-value mapping was incorrect) : 
[com.vladmihalcea.book.hpjp.hibernate.concurrency.version.Product#1]]

Обратите внимание, что ОБНОВЛЕНИЕ Алисы завершается неудачно, потому что значение столбца версия изменилось.

Версия принимается во внимание при объединении отдельного объекта, как показано в следующем примере.

Этот пример можно резюмировать следующим образом:

  1. Алиса загружает Продукт объект, который имеет количественное значение 5 и версия из 1 .
  2. Задание пакетного процессора обновляет Количество продукта до 0 и версия теперь 2 .
  3. Алиса пытается купить новый Товар , следовательно, количество Товара уменьшается.
  4. Когда Алиса попытается объединить отделенную сущность Product , будет выдано исключение OptimisticLockException , поскольку версия Product изменилась.

Следующий тестовый пример инкапсулирует всю вышеупомянутую логику:

String productJsonString = doInJPA(entityManager -> {
    return JacksonUtil.toString(
        entityManager.find(
            Product.class, 
            1L
        )
    );
});

executeSync(() -> doInJPA(entityManager -> {
    LOGGER.info("Batch processor updates product stock");

    Product product = entityManager.find(
        Product.class,
        1L
    );
    
    product.setQuantity(0);
}));

LOGGER.info("Changing the previously loaded Product entity");

ObjectNode productJsonNode = (ObjectNode) JacksonUtil
.toJsonNode(productJsonString);

int quantity  = productJsonNode.get("quantity").asInt();

productJsonNode.put(
    "quantity", 
    String.valueOf(--quantity)
);

doInJPA(entityManager -> {
    LOGGER.info("Merging the Product entity");

    Product product = JacksonUtil.fromString(
        productJsonNode.toString(),
        Product.class
    );
    entityManager.merge(product);
});

При выполнении приведенного выше тестового случая переведите в спящий режим следующие инструкции SQL:

DEBUG [Alice]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    SELECT 
        p.id as id1_0_0_, 
        p.quantity as quantity2_0_0_, 
        p.version as version3_0_0_ 
    FROM 
        product p 
    WHERE 
        p.id = ?
"], 
Params:[(
    1
)]

-- Batch processor updates product stock

DEBUG [Bob]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    SELECT 
        p.id as id1_0_0_, 
        p.quantity as quantity2_0_0_, 
        p.version as version3_0_0_ 
    FROM 
        product p 
    WHERE 
        p.id = ?
"], 
Params:[(
    1
)]

DEBUG [Bob]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    UPDATE 
        product 
    SET 
        quantity = ?, 
        version = ? 
    WHERE 
        id=? AND 
        version=?
"], 
Params:[(
    0, 
    2, 
    1, 
    1
)]

-- Changing the previously loaded Product entity

-- Merging the Product entity

DEBUG [Alice]: n.t.d.l.SLF4JQueryLoggingListener - 
Query:["
    SELECT 
        p.id as id1_0_0_, 
        p.quantity as quantity2_0_0_, 
        p.version as version3_0_0_ 
    FROM 
        product p 
    WHERE 
        p.id = ?
"], 
Params:[(
    1
)]

ERROR [Alice]: c.v.b.h.h.c.v.VersionTest - Throws
javax.persistence.OptimisticLockException: 
Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)
at org.hibernate.internal.ExceptionConverterImpl.wrapStaleStateException(ExceptionConverterImpl.java:226)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:93)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:917)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:891)

Таким образом, при попытке объединить отделенную Продукт сущность Hibernate сначала загружает текущий моментальный снимок базы данных и присоединяет состояние времени загрузки к текущему контексту сохранения. При копировании состояния отсоединенной сущности во вновь загруженную сущность Product Hibernate обнаруживает, что версия изменилась, поэтому сразу же выдает исключение OptimisticLockException , поэтому пропускает ОБНОВЛЕНИЕ , которое не удалось бы с тем же исключением.

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

Более того, если вы попытаетесь изменить версию, механизм грязной проверки запустит бесполезное ОБНОВЛЕНИЕ.

Итак, при выполнении следующего тестового случая:

Product product = entityManager.find(
    Product.class, 
    1L
);

product.setVersion(100);

Hibernate генерирует инструкцию UPDATE , которая увеличивает только версию, оставляя все остальные столбцы без изменений (их значения идентичны тем, которые были ранее загружены из базы данных).:

UPDATE 
    product 
SET 
    quantity = 5, 
    version = 2 
WHERE 
    id = 1 AND 
    version = 1

Если вы хотите принудительно изменить версию сущности, вам нужно использовать либо OPTIMISTIC_FORCE_INCREMENT, либо PESSIMISTIC_FORCE_INCREMENT .

Обратите внимание, что ОБНОВЛЕНИЕ по умолчанию включает все столбцы, связанные с текущей сущностью. Это позволяет Hibernate автоматически паковать операторы DML и также извлекать выгоду из кэширования операторов .

Если вы хотите, чтобы инструкция UPDATE включала только измененные столбцы, вам необходимо использовать аннотацию @DynamicUpdate на уровне сущности.

Аннотация @Version позволяет Hibernate активировать механизм оптимистической блокировки при выполнении ОБНОВЛЕНИЯ или УДАЛЕНИЯ оператора в отношении рассматриваемой сущности.

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