Автор оригинала: 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
Чтобы понять, как свойство версии может помочь предотвратить потерю обновлений, рассмотрим следующий пример:
Этот пример можно резюмировать следующим образом:
- Алиса загружает
Продукт
объект, который имеет количественное значение5
иверсия
из1
. - Задание пакетного процессора обновляет
Количество продукта
до0
иверсия
теперь2
. - Алиса пытается купить новый
Товар
, следовательно, количествоТовара
уменьшается. - Когда
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]]
Обратите внимание, что ОБНОВЛЕНИЕ
Алисы завершается неудачно, потому что значение столбца версия
изменилось.
Версия
принимается во внимание при объединении отдельного объекта, как показано в следующем примере.
Этот пример можно резюмировать следующим образом:
- Алиса загружает
Продукт
объект, который имеет количественное значение5
иверсия
из1
. - Задание пакетного процессора обновляет
Количество продукта
до0
иверсия
теперь2
. - Алиса пытается купить новый
Товар
, следовательно, количествоТовара
уменьшается. - Когда Алиса попытается объединить отделенную сущность
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 активировать механизм оптимистической блокировки при выполнении ОБНОВЛЕНИЯ
или УДАЛЕНИЯ
оператора в отношении рассматриваемой сущности.
Используя механизм оптимистической блокировки, вы можете предотвратить потерю обновлений как при подключении сущности к текущему контексту сохранения, так и при изменении сущности в отключенном состоянии.