From 839bba9365194ecf01b29172d228cddb8b45f302 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Fri, 8 Dec 2023 14:50:30 +0100 Subject: [PATCH] Change semantic of userObjects in nested transaction --- .../java/io/ebeaninternal/api/ScopeTrans.java | 20 +++++++++++ .../ebeaninternal/api/ScopedTransaction.java | 28 +++++++++++++++- .../ebeaninternal/server/util/ArrayStack.java | 8 ++++- .../transaction/TestExecuteComplete.java | 33 ++++++++++++++++--- 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/ScopeTrans.java b/ebean-core/src/main/java/io/ebeaninternal/api/ScopeTrans.java index b35ad5ed26..1dbedb6eb9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/ScopeTrans.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/ScopeTrans.java @@ -3,6 +3,8 @@ import io.ebean.TxScope; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; /** * Used internally to handle the scoping of transactions for methods. @@ -47,6 +49,11 @@ public final class ScopeTrans { private boolean nestedCommit; private boolean nestedUseSavepoint; + /** + * The UserObjects when using in nested transactions. + */ + private Map userObjects; + public ScopeTrans(boolean rollbackOnChecked, boolean created, SpiTransaction transaction, TxScope txScope) { this.rollbackOnChecked = rollbackOnChecked; this.created = created; @@ -218,4 +225,17 @@ private boolean isRollbackThrowable(Throwable e) { return e instanceof RuntimeException || rollbackOnChecked; } + public void putUserObject(String name, Object value) { + if (userObjects == null) { + userObjects = new HashMap<>(); + } + userObjects.put(name, value); + } + + public Object getUserObject(String name) { + if (userObjects == null) { + return null; + } + return userObjects.get(name); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java index ca2cf7dde0..9f46c97a7f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java @@ -2,7 +2,6 @@ import io.ebeaninternal.server.transaction.TransactionScopeManager; import io.ebeaninternal.server.util.ArrayStack; - import jakarta.persistence.PersistenceException; /** @@ -173,4 +172,31 @@ public Exception caughtThrowable(Exception e) { return current.caughtThrowable(e); } + /** + * New user objects are always written to the current ScopeTrans. + */ + @Override + public void putUserObject(String name, Object value) { + current.putUserObject(name, value); + } + + /** + * Returns the userObject in the stack, Herew we search + * the stack and return the first found userObject + */ + @Override + public Object getUserObject(String name) { + Object obj = current.getUserObject(name); + if (obj != null) { + return obj; + } + for (ScopeTrans trans : stack) { + obj = trans.getUserObject(name); + if (obj != null) { + return obj; + } + } + return transaction.getUserObject(name); + } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/util/ArrayStack.java b/ebean-core/src/main/java/io/ebeaninternal/server/util/ArrayStack.java index 4e76caea86..ece602b0fd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/util/ArrayStack.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/util/ArrayStack.java @@ -2,12 +2,13 @@ import java.util.ArrayList; import java.util.EmptyStackException; +import java.util.Iterator; import java.util.List; /** * Stack based on ArrayList. */ -public class ArrayStack { +public class ArrayStack implements Iterable { private final List list; @@ -89,4 +90,9 @@ public int size() { public boolean contains(E o) { return list.contains(o); } + + @Override + public Iterator iterator() { + return list.iterator(); + } } diff --git a/ebean-test/src/test/java/org/tests/transaction/TestExecuteComplete.java b/ebean-test/src/test/java/org/tests/transaction/TestExecuteComplete.java index 2b95e9d6f2..23baa3e8be 100644 --- a/ebean-test/src/test/java/org/tests/transaction/TestExecuteComplete.java +++ b/ebean-test/src/test/java/org/tests/transaction/TestExecuteComplete.java @@ -1,11 +1,14 @@ package org.tests.transaction; -import io.ebean.*; -import io.ebean.xtest.BaseTestCase; -import io.ebean.xtest.ForPlatform; +import io.ebean.DB; +import io.ebean.DataIntegrityException; +import io.ebean.Transaction; +import io.ebean.TxScope; import io.ebean.annotation.PersistBatch; import io.ebean.annotation.Platform; import io.ebean.annotation.Transactional; +import io.ebean.xtest.BaseTestCase; +import io.ebean.xtest.ForPlatform; import io.ebeaninternal.api.SpiTransaction; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; @@ -43,7 +46,7 @@ public void nestedExecute_when_errorOnCommit_threadLocalIsCleared() { try { DB.execute(TxScope.required().setBatch(PersistBatch.ALL), () -> - DB.execute(() -> { + DB.execute(() -> { Customer customer = DB.reference(Customer.class, 42424242L); Order order = new Order(); @@ -158,4 +161,26 @@ public void no_transaction_expect_threadScopeCleanup() { assertThat(getInScopeTransaction()).isNull(); } + @ForPlatform(Platform.H2) + @Test + public void test_nested_userobjects() { + + try (Transaction txn1 = DB.beginTransaction()) { + assertThat(getInScopeTransaction()).isNotNull(); + getInScopeTransaction().putUserObject("foo", "bar"); + + try (Transaction txn2 = DB.beginTransaction()) { + assertThat(getInScopeTransaction().getUserObject("foo")).isEqualTo("bar"); + getInScopeTransaction().putUserObject("foo", "xxx"); + getInScopeTransaction().putUserObject("test", "xxx"); + assertThat(getInScopeTransaction().getUserObject("foo")).isEqualTo("xxx"); + txn2.commit(); + } + // CHECKME: What would we expect here? I would expect "bar" but get "xxx" + // NOTE: with TxScope.requiresNew() - I'll get "bar" + assertThat(getInScopeTransaction().getUserObject("test")).isNull(); + assertThat(getInScopeTransaction().getUserObject("foo")).isEqualTo("bar"); + } + } + }