From 9fc2ec044d0a99e39550ff9474c77d65447b800d Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 9 Sep 2021 18:18:16 +1200 Subject: [PATCH] #2362 - ENH: Add lock(bean) method as convenience for DB pessimistic locking --- ebean-api/src/main/java/io/ebean/DB.java | 15 +++++ .../src/main/java/io/ebean/Database.java | 14 ++++ .../server/core/DefaultServer.java | 25 ++++--- .../io/ebeaninternal/api/TDSpiServer.java | 4 ++ .../org/tests/basic/TestQueryForUpdate.java | 66 ++++++++++++++++++- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/DB.java b/ebean-api/src/main/java/io/ebean/DB.java index 7c8523648..9bcfb047d 100644 --- a/ebean-api/src/main/java/io/ebean/DB.java +++ b/ebean-api/src/main/java/io/ebean/DB.java @@ -1272,6 +1272,21 @@ public final class DB { return beanId(bean); } + /** + * Load and lock the bean using {@code select for update}. + *

+ * This should be executed inside a transaction. + *

+ * The bean needs to have an ID property set and can be a reference bean (only has ID) + * or partially or fully populated bean. This will load all the properties of the bean + * from the database using {@code select for update}. + * + * @param bean The entity bean that we wish to obtain a database lock on. + */ + public static void lock(Object bean) { + getDefault().lock(bean); + } + /** * Deprecated migrate to cacheManager(). */ diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java index 594644c62..396f3225a 100644 --- a/ebean-api/src/main/java/io/ebean/Database.java +++ b/ebean-api/src/main/java/io/ebean/Database.java @@ -1714,6 +1714,20 @@ public interface Database { */ Set validateQuery(Query query); + /** + * Load and lock the bean using {@code select for update}. + *

+ * This should be executed inside a transaction and results in the bean being loaded or + * refreshed from the database and a database row lock held via {@code select for update}. + *

+ * The bean needs to have an ID property set and can be a reference bean (only has ID) + * or partially or fully populated bean. This will load all the properties of the bean + * from the database using {@code select for update} obtaining a database row lock (using WAIT). + * + * @param bean The entity bean that we wish to obtain a database lock on. + */ + void lock(Object bean); + /** * Truncate all the given tables. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 576ee5add..0e293d37d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -129,16 +129,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.time.Clock; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.Spliterator; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -930,6 +921,20 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { executeInTrans((txn) -> persister.merge(desc, checkEntityBean(bean), options, txn), transaction); } + @Override + public void lock(Object bean) { + BeanDescriptor desc = descriptor(bean.getClass()); + if (desc == null) { + throw new PersistenceException(bean.getClass() + " is NOT an Entity Bean registered with this server?"); + } + Object id = desc.id(bean); + Objects.requireNonNull(id, "Bean missing an @Id value which is required to lock"); + new DefaultOrmQuery<>(desc, this, expressionFactory) + .setId(id) + .withLock(Query.LockType.DEFAULT, Query.LockWait.NOWAIT) + .findOne(); + } + @Override public Query find(Class beanType) { return createQuery(beanType); diff --git a/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java b/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java index 2ea3b617d..620e9664a 100644 --- a/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java +++ b/ebean-test/src/test/java/io/ebeaninternal/api/TDSpiServer.java @@ -137,6 +137,10 @@ public class TDSpiServer implements SpiServer { return null; } + @Override + public void lock(Object bean) { + } + @Override public Object nextId(Class beanType) { return null; diff --git a/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java b/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java index 1347da7e2..527146367 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java +++ b/ebean-test/src/test/java/org/tests/basic/TestQueryForUpdate.java @@ -7,12 +7,13 @@ import io.ebean.test.LoggedSql; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.EBasic; +import org.tests.model.basic.Order; import org.tests.model.basic.ResetBasicData; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class TestQueryForUpdate extends BaseTestCase { @@ -40,6 +41,69 @@ public class TestQueryForUpdate extends BaseTestCase { } } + @Test + @ForPlatform({ Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB}) + public void testForUpdate_when_alreadyInPCAsReference() { + ResetBasicData.reset(); + Order o0 = DB.find(Order.class).orderBy("id").setMaxRows(1).findOne(); + Integer customerId = o0.getCustomer().getId(); + + try (Transaction transaction = DB.beginTransaction()) { + + LoggedSql.start(); + Order order = DB.find(Order.class, o0.getId()); + assert order != null; + + Customer customer = order.getCustomer(); + assertTrue(DB.beanState(customer).isReference()); + assertEquals(customerId, customer.getId()); + + Customer customer1 = DB.find(Customer.class).where().idEq(customer.getId()).forUpdate().findOne(); + + assertThat(customer).isSameAs(customer1); + assertThat(customer.getName()).isNotNull(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("from o_order"); + assertThat(sql.get(1)).contains("from o_customer t0 where t0.id = ? for update"); + + transaction.commit(); + } + } + + @Test + @ForPlatform({ Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB}) + public void testForUpdate_when_alreadyInPCAsReference_usingLock() { + ResetBasicData.reset(); + Order o0 = DB.find(Order.class).orderBy("id").setMaxRows(1).findOne(); + Integer customerId = o0.getCustomer().getId(); + + try (Transaction transaction = DB.beginTransaction()) { + + LoggedSql.start(); + Order order = DB.find(Order.class, o0.getId()); + assert order != null; + + Customer customer = order.getCustomer(); + assertEquals(customerId, customer.getId()); + assertTrue(DB.beanState(customer).isReference()); + + DB.lock(customer); // load the customer bean using select for update + // bean is now loaded and database row lock held until commit + assertFalse(DB.beanState(customer).isReference()); + + assertThat(customer.getName()).isNotNull(); + + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("from o_order"); + assertThat(sql.get(1)).contains("from o_customer t0 where t0.id = ? for update"); + + transaction.commit(); + } + } + @Test @ForPlatform({ Platform.H2, Platform.ORACLE, Platform.POSTGRES, Platform.SQLSERVER, Platform.MYSQL, Platform.MARIADB}) public void testForUpdate_when_alreadyInPC() {