Bug 578262: Bulk update queries reuse the same version locking value
Signed-off-by: Will Dazey <dazeydev.3@gmail.com>
diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java
index 0328762..064cef3 100644
--- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java
+++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/descriptors/TimestampLockingPolicy.java
@@ -1,5 +1,6 @@
/*
- * Copyright (c) 1998, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022 IBM Corporation. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -206,7 +207,7 @@
*/
@Override
public Expression getWriteLockUpdateExpression(ExpressionBuilder builder, AbstractSession session) {
- return builder.value(getInitialWriteValue(session));
+ return builder.currentTimeStamp();
}
/**
diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java
index 3783ab2..f41c2c0 100644
--- a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java
+++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/TestTimestampVersionLocking.java
@@ -1,6 +1,6 @@
/*
- * Copyright (c) 2015, 2021 Oracle and/or its affiliates. All rights reserved.
- * Copyright (c) 2015 IBM Corporation. All rights reserved.
+ * Copyright (c) 2015, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2022 IBM Corporation. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -18,6 +18,9 @@
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.Query;
+
+import java.sql.Timestamp;
import org.eclipse.persistence.descriptors.TimestampLockingPolicy;
import org.eclipse.persistence.internal.jpa.EntityManagerImpl;
@@ -26,6 +29,7 @@
import org.eclipse.persistence.jpa.test.framework.EmfRunner;
import org.eclipse.persistence.jpa.test.framework.Property;
import org.eclipse.persistence.jpa.test.locking.model.ClassDescriptorCustomizer;
+import org.eclipse.persistence.jpa.test.locking.model.EcallRegistration;
import org.eclipse.persistence.jpa.test.locking.model.TimestampDog;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.sessions.Session;
@@ -59,6 +63,9 @@
@Property(name = "eclipselink.descriptor.customizer.TimestampDog", value = "org.eclipse.persistence.jpa.test.locking.model.ClassDescriptorCustomizer") })
private EntityManagerFactory emfFalseCustomized;
+ @Emf(name = "emf2", createTables = DDLGen.DROP_CREATE, classes = { EcallRegistration.class })
+ private EntityManagerFactory emf2;
+
/**
* Check that setting the property "true" will get the local system time
* instead of the default behavior of contacting the server.
@@ -204,6 +211,200 @@
}
/**
+ * Test bulk update queries do not reuse the same timestamp value across multiple executions
+ */
+ @Test
+ public void testUpdateAllQueryWithTimestampLocking() {
+
+ int flag1 = 0;
+ int flag2 = 1;
+ String pk1 = "11004";
+ String pk2 = "11005";
+
+ // Populate entities with initial timestamp version values
+ EntityManager em = emf2.createEntityManager();
+ try {
+ em.getTransaction().begin();
+
+ EcallRegistration e1 = new EcallRegistration(pk1, flag1);
+ EcallRegistration e2 = new EcallRegistration(pk2, flag2);
+
+ em.persist(e1);
+ em.persist(e2);
+
+ em.getTransaction().commit();
+ } finally {
+ if(em.isOpen()) {
+ em.clear();
+ em.close();
+ }
+ }
+
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ /*
+ * Update flag1 value via bulk Update query.
+ * This should update the version locking timestamp value for entity1.
+ */
+ em = emf2.createEntityManager();
+ try {
+ flag1 = flag1++;
+
+ em.getTransaction().begin();
+
+ Query query = em.createNamedQuery("updateActiveEcallAvailableFlag", EcallRegistration.class);
+ query.setParameter("flag", flag1);
+ query.setParameter("pk", pk1);
+
+ query.executeUpdate();
+
+ em.getTransaction().commit();
+ } finally {
+ if(em.isOpen()) {
+ em.clear();
+ em.close();
+ }
+ }
+
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ /*
+ * Update flag2 value via bulk Update query.
+ * This should update the version locking timestamp value for entity2.
+ */
+ em = emf2.createEntityManager();
+ try {
+ flag2 = flag2++;
+
+ em.getTransaction().begin();
+
+ Query query = em.createNamedQuery("updateActiveEcallAvailableFlag", EcallRegistration.class);
+ query.setParameter("flag", flag2);
+ query.setParameter("pk", pk2);
+
+ query.executeUpdate();
+
+ em.getTransaction().commit();
+ } finally {
+ if(em.isOpen()) {
+ em.clear();
+ em.close();
+ }
+ }
+
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ /*
+ * Validate that even though both bulk updates used the same UpdateAllQuery, both entities have
+ * different version locking timestamps in the database.
+ */
+ em = emf2.createEntityManager();
+ try {
+ em.getTransaction().begin();
+
+ EcallRegistration e1 = em.find(EcallRegistration.class, pk1);
+ EcallRegistration e2 = em.find(EcallRegistration.class, pk2);
+
+ Assert.assertNotNull(e1);
+ Assert.assertNotNull(e2);
+
+ Assert.assertNotEquals(e1.getSysUpdateTimestamp(), e2.getSysUpdateTimestamp());
+ Assert.assertTrue("Expected entity2.sysUpdateTimestamp [" + e2.getSysUpdateTimestamp() + "] "
+ + "to be after entity1.sysUpdateTimestamp [" + e1.getSysUpdateTimestamp() + "]",
+ e2.getSysUpdateTimestamp().after(e1.getSysUpdateTimestamp()));
+
+ em.getTransaction().commit();
+ } finally {
+ if(em.isOpen()) {
+ em.clear();
+ em.close();
+ }
+ }
+ }
+
+ /**
+ * Test that bulk update queries do not update the managed entities version
+ * values as documented in the specification
+ *
+ * JPA Spec section 4.10: Bulk Update and Delete Operations
+ *
+ * Bulk update maps directly to a database update operation, bypassing optimistic locking checks.
+ * Portable applications must manually update the value of the version column, if desired, and/or
+ * manually validate the value of the version column.
+ */
+ @Test
+ public void testTimestampLockingUpdateWithUpdateAllQuery() {
+
+ int flag1 = 0;
+ String pk1 = "11006";
+
+ EntityManager em = emf2.createEntityManager();
+ try {
+ // Populate an entity
+ em.getTransaction().begin();
+
+ EcallRegistration persist1 = new EcallRegistration(pk1, flag1);
+
+ em.persist(persist1);
+
+ em.getTransaction().commit();
+
+ // Find managed instance, record current timestamp version value
+ em.getTransaction().begin();
+
+ EcallRegistration find1 = em.find(EcallRegistration.class, pk1);
+ Assert.assertNotNull(find1);
+
+ Timestamp ver1 = find1.getSysUpdateTimestamp();
+ Assert.assertNotNull(ver1);
+
+ em.getTransaction().commit();
+
+ // Execute UpdateAllQuery to update version locking field in db
+ flag1 = flag1++;
+
+ em.getTransaction().begin();
+
+ Query query = em.createNamedQuery("updateActiveEcallAvailableFlag", EcallRegistration.class);
+ query.setParameter("flag", flag1);
+ query.setParameter("pk", pk1);
+
+ query.executeUpdate();
+
+ em.getTransaction().commit();
+
+ // Verify that the UpdateAllQuery didn't update the managed instance
+ em.getTransaction().begin();
+
+ EcallRegistration find2 = em.find(EcallRegistration.class, pk1);
+ Assert.assertNotNull(find2);
+
+ Timestamp ver2 = find2.getSysUpdateTimestamp();
+ Assert.assertNotNull(ver2);
+ Assert.assertEquals(ver1, ver2);
+
+ em.getTransaction().commit();
+ } finally {
+ if(em.isOpen()) {
+ em.clear();
+ em.close();
+ }
+ }
+ }
+
+ /**
* Add this to Session Event Manager to listen for query executions. This
* class will listen for the execution of a specified query and track it.
*/
diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/model/EcallRegistration.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/model/EcallRegistration.java
new file mode 100644
index 0000000..0fd8655
--- /dev/null
+++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/locking/model/EcallRegistration.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022 IBM Corporation. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+package org.eclipse.persistence.jpa.test.locking.model;
+
+import java.sql.Timestamp;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.NamedQuery;
+import jakarta.persistence.Version;
+
+@Entity
+@NamedQuery(name="updateActiveEcallAvailableFlag", query="UPDATE EcallRegistration e SET e.ecallAvailableFlag = :flag WHERE e.pk = :pk")
+public class EcallRegistration {
+
+ @Id private String pk;
+
+ private int ecallAvailableFlag;
+
+ @Version @Column(name = "sys_update_timestamp")
+ private Timestamp sysUpdateTimestamp;
+
+ public EcallRegistration() { }
+
+ public EcallRegistration(String pk, int ecallAvailableFlag) {
+ this.pk = pk;
+ this.ecallAvailableFlag = ecallAvailableFlag;
+ }
+
+ public String getPk() {
+ return pk;
+ }
+
+ public void setPk(String pk) {
+ this.pk = pk;
+ }
+
+ public int getEcallAvailableFlag() {
+ return ecallAvailableFlag;
+ }
+
+ public void setEcallAvailableFlag(int ecallAvailableFlag) {
+ this.ecallAvailableFlag = ecallAvailableFlag;
+ }
+
+ public Timestamp getSysUpdateTimestamp() {
+ return this.sysUpdateTimestamp;
+ }
+
+ public void setSysUpdateTimestamp(Timestamp sysUpdateTimestamp) {
+ this.sysUpdateTimestamp = sysUpdateTimestamp;
+ }
+}