From f7fa778e99c0e6d4e11247d9491ad51c5ce9586f Mon Sep 17 00:00:00 2001 From: rbygrave Date: Fri, 8 May 2015 21:55:49 +1200 Subject: [PATCH] #287 - ENH: Support nested transactions with the Ebean.beginTransaction() API --- src/main/java/com/avaje/ebean/Ebean.java | 55 +++ .../java/com/avaje/ebean/EbeanServer.java | 53 +++ .../avaje/ebeaninternal/api/ScopeTrans.java | 57 +-- .../ebeaninternal/api/ScopedTransaction.java | 324 ++++++++++++++++++ .../server/core/DefaultServer.java | 5 + .../ebeaninternal/api/TDSpiEbeanServer.java | 5 + .../transaction/TestNestedBeginRequired.java | 58 ++++ .../TestNestedBeginRequiredWithFailure.java | 70 ++++ .../TestNestedBeginRequiresNew.java | 57 +++ ...TestNestedBeginRequiresNewWithFailure.java | 63 ++++ 10 files changed, 726 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java create mode 100644 src/test/java/com/avaje/tests/transaction/TestNestedBeginRequired.java create mode 100644 src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiredWithFailure.java create mode 100644 src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNew.java create mode 100644 src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNewWithFailure.java diff --git a/src/main/java/com/avaje/ebean/Ebean.java b/src/main/java/com/avaje/ebean/Ebean.java index 7d21a09cf..006753624 100644 --- a/src/main/java/com/avaje/ebean/Ebean.java +++ b/src/main/java/com/avaje/ebean/Ebean.java @@ -370,6 +370,61 @@ public final class Ebean { return serverMgr.getPrimaryServer().beginTransaction(isolation); } + /** + * Start a transaction typically specifying REQUIRES_NEW or REQUIRED semantics. + * + *

+ * Note that this provides an try finally alternative to using {@link #execute(TxScope, TxCallable)} or + * {@link #execute(TxScope, TxRunnable)}. + *

+ * + *

REQUIRES_NEW example:

+ *
{@code
+   * // Start a new transaction. If there is a current transaction
+   * // suspend it until this transaction ends
+   * Transaction txn = Ebean.beginTransaction(TxScope.requiresNew());
+   * try {
+   *
+   *   ...
+   *
+   *   // commit the transaction
+   *   txn.commit();
+   *
+   * } finally {
+   *   // end this transaction which:
+   *   //  A) will rollback transaction if it has not been committed already
+   *   //  B) will restore a previously suspended transaction
+   *   txn.end();
+   * }
+   *
+   * }
+ * + *

REQUIRED example:

+ *
{@code
+   *
+   * // start a new transaction if there is not a current transaction
+   * Transaction txn = Ebean.beginTransaction(TxScope.required());
+   * try {
+   *
+   *   ...
+   *
+   *   // commit the transaction if it was created or
+   *   // do nothing if there was already a current transaction
+   *   txn.commit();
+   *
+   * } finally {
+   *   // end this transaction which will rollback the transaction
+   *   // if it was created for this try finally scope and has not
+   *   // already been committed
+   *   txn.end();
+   * }
+   *
+   * }
+ */ + public static Transaction beginTransaction(TxScope scope){ + return serverMgr.getPrimaryServer().beginTransaction(scope); + } + /** * Returns the current transaction or null if there is no current transaction * in scope. diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index cf855e281..32712f8b4 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -586,6 +586,59 @@ public interface EbeanServer { */ public Transaction beginTransaction(TxIsolation isolation); + /** + * Start a transaction typically specifying REQUIRES_NEW or REQUIRED semantics. + * + *

+ * Note that this provides an try finally alternative to using {@link #execute(TxScope, TxCallable)} or + * {@link #execute(TxScope, TxRunnable)}. + *

+ * + *

REQUIRES_NEW example:

+ *
{@code
+   * // Start a new transaction. If there is a current transaction
+   * // suspend it until this transaction ends
+   * Transaction txn = server.beginTransaction(TxScope.requiresNew());
+   * try {
+   *
+   *   ...
+   *
+   *   // commit the transaction
+   *   txn.commit();
+   *
+   * } finally {
+   *   // end this transaction which:
+   *   //  A) will rollback transaction if it has not been committed already
+   *   //  B) will restore a previously suspended transaction
+   *   txn.end();
+   * }
+   *
+   * }
+ * + *

REQUIRED example:

+ *
{@code
+   *
+   * // start a new transaction if there is not a current transaction
+   * Transaction txn = server.beginTransaction(TxScope.required());
+   * try {
+   *
+   *   ...
+   *
+   *   // commit the transaction if it was created or
+   *   // do nothing if there was already a current transaction
+   *   txn.commit();
+   *
+   * } finally {
+   *   // end this transaction which will rollback the transaction
+   *   // if it was created for this try finally scope and has not
+   *   // already been committed
+   *   txn.end();
+   * }
+   *
+   * }
+ */ + public Transaction beginTransaction(TxScope scope); + /** * Returns the current transaction or null if there is no current transaction in scope. */ diff --git a/src/main/java/com/avaje/ebeaninternal/api/ScopeTrans.java b/src/main/java/com/avaje/ebeaninternal/api/ScopeTrans.java index bfca3fabf..462eac317 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/ScopeTrans.java +++ b/src/main/java/com/avaje/ebeaninternal/api/ScopeTrans.java @@ -87,7 +87,14 @@ public class ScopeTrans implements Thread.UncaughtExceptionHandler { } } - + + /** + * Return the current/active transaction. + */ + protected SpiTransaction getTransaction() { + return transaction; + } + /** * Called when the Thread catches any uncaught exception. * For example, an unexpected NullPointerException or Error. @@ -121,32 +128,40 @@ public class ScopeTrans implements Thread.UncaughtExceptionHandler { * Also reinstate the suspended transaction if there was one. */ public void onFinally() { + try { if (!rolledBack) { - if (created) { - transaction.commit(); - } else { - if (restoreBatch != null) { - transaction.setBatch(restoreBatch); - } - if (restoreBatchOnCascade != null) { - transaction.setBatchOnCascade(restoreBatchOnCascade); - } - if (restoreBatchSize > 0) { - transaction.setBatchSize(restoreBatchSize); - } - } + commitTransaction(); } - } finally { - if (suspendedTransaction != null){ - // put the previously suspended transaction - // back onto the ThreadLocal or equivalent - scopeMgr.replace(suspendedTransaction); - } + restoreSuspended(); } } + protected void restoreSuspended() { + if (suspendedTransaction != null){ + // put the previously suspended transaction + // back onto the ThreadLocal or equivalent + scopeMgr.replace(suspendedTransaction); + } + } + + protected void commitTransaction() { + if (created) { + transaction.commit(); + } else { + if (restoreBatch != null) { + transaction.setBatch(restoreBatch); + } + if (restoreBatchOnCascade != null) { + transaction.setBatchOnCascade(restoreBatchOnCascade); + } + if (restoreBatchSize > 0) { + transaction.setBatchSize(restoreBatchSize); + } + } + } + /** * An Error was caught and this ALWAYS causes a rollback to occur. * Returns the error and this should be thrown by the calling code. @@ -168,7 +183,7 @@ public class ScopeTrans implements Thread.UncaughtExceptionHandler { return e; } - private void rollback(Throwable e) { + protected void rollback(Throwable e) { if (transaction != null && transaction.isActive()) { // transaction is null for NOT_SUPPORTED and sometimes SUPPORTS // and Inactive (already rolled back) if nested REQUIRED diff --git a/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java b/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java new file mode 100644 index 000000000..3b0b85b20 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java @@ -0,0 +1,324 @@ +package com.avaje.ebeaninternal.api; + +import com.avaje.ebean.TransactionCallback; +import com.avaje.ebean.bean.PersistenceContext; +import com.avaje.ebean.config.PersistBatch; +import com.avaje.ebeaninternal.server.core.PersistRequest; +import com.avaje.ebeaninternal.server.core.PersistRequestBean; +import com.avaje.ebeaninternal.server.persist.BatchControl; + +import javax.persistence.PersistenceException; +import javax.persistence.RollbackException; +import java.io.IOException; +import java.sql.Connection; +import java.util.List; + +/** + * Wrapper of a ScopeTrans request and it's underlying transaction. + */ +public class ScopedTransaction implements SpiTransaction { + + final ScopeTrans scopeTrans; + + final SpiTransaction transaction; + + boolean committed; + + public ScopedTransaction(ScopeTrans scopeTrans) { + this.scopeTrans = scopeTrans; + this.transaction =scopeTrans.getTransaction(); + } + + + @Override + public void commit() throws RollbackException { + scopeTrans.commitTransaction(); + committed = true; + } + + @Override + public void rollback() throws PersistenceException { + scopeTrans.rollback(null); + } + + @Override + public void rollback(Throwable e) throws PersistenceException { + scopeTrans.rollback(e); + } + + @Override + public void end() throws PersistenceException { + try { + if (!committed) { + scopeTrans.rollback(null); + } + } finally { + scopeTrans.restoreSuspended(); + } + } + + @Override + public void endQueryOnly() { + transaction.endQueryOnly(); + } + + @Override + public String getLogPrefix() { + return transaction.getLogPrefix(); + } + + @Override + public boolean isLogSql() { + return transaction.isLogSql(); + } + + @Override + public boolean isLogSummary() { + return transaction.isLogSummary(); + } + + @Override + public void logSql(String msg) { + transaction.logSql(msg); + } + + @Override + public void logSummary(String msg) { + transaction.logSummary(msg); + } + + @Override + public void registerDerivedRelationship(DerivedRelationshipData assocBean) { + transaction.registerDerivedRelationship(assocBean); + } + + @Override + public List getDerivedRelationship(Object bean) { + return transaction.getDerivedRelationship(bean); + } + + @Override + public void registerDeleteBean(Integer hash) { + transaction.registerDeleteBean(hash); + } + + @Override + public void unregisterDeleteBean(Integer hash) { + transaction.unregisterDeleteBean(hash); + } + + @Override + public boolean isRegisteredDeleteBean(Integer hash) { + return transaction.isRegisteredDeleteBean(hash); + } + + @Override + public void unregisterBean(Object bean) { + transaction.unregisterBean(bean); + } + + @Override + public boolean isRegisteredBean(Object bean) { + return transaction.isRegisteredBean(bean); + } + + @Override + public String getId() { + return transaction.getId(); + } + + @Override + public void register(TransactionCallback callback) { + transaction.register(callback); + } + + @Override + public boolean isReadOnly() { + return transaction.isReadOnly(); + } + + @Override + public void setReadOnly(boolean readOnly) { + transaction.setReadOnly(readOnly); + } + + @Override + public boolean isActive() { + return transaction.isActive(); + } + + @Override + public void setPersistCascade(boolean persistCascade) { + transaction.setPersistCascade(persistCascade); + } + + @Override + public void setBatchMode(boolean useBatch) { + transaction.setBatchMode(useBatch); + } + + @Override + public void setBatch(PersistBatch persistBatchMode) { + transaction.setBatch(persistBatchMode); + } + + @Override + public PersistBatch getBatch() { + return transaction.getBatch(); + } + + @Override + public void setBatchOnCascade(PersistBatch batchOnCascadeMode) { + transaction.setBatchOnCascade(batchOnCascadeMode); + } + + @Override + public PersistBatch getBatchOnCascade() { + return transaction.getBatchOnCascade(); + } + + @Override + public void setBatchSize(int batchSize) { + transaction.setBatchSize(batchSize); + } + + @Override + public int getBatchSize() { + return transaction.getBatchSize(); + } + + @Override + public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) { + transaction.setBatchGetGeneratedKeys(getGeneratedKeys); + } + + @Override + public void setBatchFlushOnMixed(boolean batchFlushOnMixed) { + transaction.setBatchFlushOnMixed(batchFlushOnMixed); + } + + @Override + public void setBatchFlushOnQuery(boolean batchFlushOnQuery) { + transaction.setBatchFlushOnQuery(batchFlushOnQuery); + } + + @Override + public boolean isBatchFlushOnQuery() { + return transaction.isBatchFlushOnQuery(); + } + + @Override + public void flushBatch() throws PersistenceException { + transaction.flushBatch(); + } + + @Override + public Connection getConnection() { + return transaction.getConnection(); + } + + @Override + public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) { + transaction.addModification(tableName, inserts, updates, deletes); + } + + @Override + public void putUserObject(String name, Object value) { + transaction.putUserObject(name, value); + } + + @Override + public Object getUserObject(String name) { + return transaction.getUserObject(name); + } + + @Override + public int depth(int diff) { + return transaction.depth(); + } + + @Override + public int depth() { + return transaction.depth(); + } + + @Override + public boolean isExplicit() { + return transaction.isExplicit(); + } + + @Override + public TransactionEvent getEvent() { + return transaction.getEvent(); + } + + @Override + public boolean isPersistCascade() { + return transaction.isPersistCascade(); + } + + @Override + public boolean isBatchThisRequest(PersistRequest.Type type) { + return transaction.isBatchThisRequest(type); + } + + @Override + public BatchControl getBatchControl() { + return transaction.getBatchControl(); + } + + @Override + public void setBatchControl(BatchControl control) { + transaction.setBatchControl(control); + } + + @Override + public PersistenceContext getPersistenceContext() { + return transaction.getPersistenceContext(); + } + + @Override + public void setPersistenceContext(PersistenceContext context) { + transaction.setPersistenceContext(context); + } + + @Override + public Connection getInternalConnection() { + return transaction.getInternalConnection(); + } + + @Override + public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) { + return transaction.isSaveAssocManyIntersection(intersectionTable, beanName); + } + + @Override + public boolean checkBatchEscalationOnCascade(PersistRequestBean request) { + return transaction.checkBatchEscalationOnCascade(request); + } + + @Override + public void flushBatchOnCascade() { + transaction.flushBatchOnCascade(); + } + + @Override + public void markNotQueryOnly() { + transaction.markNotQueryOnly(); + } + + @Override + public void checkBatchEscalationOnCollection() { + transaction.checkBatchEscalationOnCollection(); + } + + @Override + public void flushBatchOnCollection() { + transaction.flushBatchOnCollection(); + } + + @Override + public void close() throws IOException { + transaction.close(); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index b4acc3a4a..61050846b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -768,6 +768,11 @@ public final class DefaultServer implements SpiEbeanServer { return t; } + public Transaction beginTransaction(TxScope scope) { + ScopeTrans scopeTrans = createScopeTrans(scope); + return new ScopedTransaction(scopeTrans); + } + /** * Start a transaction with a specific Isolation Level. *

diff --git a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java index 4fb8f035f..1fabf523f 100644 --- a/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java +++ b/src/test/java/com/avaje/ebeaninternal/api/TDSpiEbeanServer.java @@ -338,6 +338,11 @@ public class TDSpiEbeanServer implements SpiEbeanServer { return null; } + @Override + public Transaction beginTransaction(TxScope scope) { + return null; + } + @Override public Transaction beginTransaction(TxIsolation isolation) { return null; diff --git a/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequired.java b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequired.java new file mode 100644 index 000000000..7782712f7 --- /dev/null +++ b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequired.java @@ -0,0 +1,58 @@ +package com.avaje.tests.transaction; + +import com.avaje.ebean.*; +import com.avaje.tests.model.basic.Country; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.Product; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestNestedBeginRequired extends BaseTestCase { + + Logger logger = LoggerFactory.getLogger(TestNestedBeginRequired.class); + + EbeanServer server = Ebean.getServer(null); + + @Test + public void test() { + + someOuterMethod(); + } + + private void someOuterMethod() { + + Transaction txn = server.beginTransaction(TxScope.required()); + try { + + server.find(Country.class).findRowCount(); + + someInnerMethod(); + + server.find(Product.class).findRowCount(); + + txn.commit(); + + } finally { + txn.end(); + } + + } + + private void someInnerMethod() { + + logger.debug("someInnerMethod() ..."); + Transaction txn = server.beginTransaction(TxScope.required()); + try { + + server.find(Customer.class).findRowCount(); + + txn.commit(); + + } finally { + txn.end(); + } + logger.debug("someInnerMethod() ... done"); + + } +} diff --git a/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiredWithFailure.java b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiredWithFailure.java new file mode 100644 index 000000000..7b3945ed8 --- /dev/null +++ b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiredWithFailure.java @@ -0,0 +1,70 @@ +package com.avaje.tests.transaction; + +import com.avaje.ebean.*; +import com.avaje.tests.model.basic.*; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestNestedBeginRequiredWithFailure extends BaseTestCase { + + Logger logger = LoggerFactory.getLogger(TestNestedBeginRequiredWithFailure.class); + + EbeanServer server = Ebean.getServer(null); + + @Test + public void test() { + + someOuterMethod(); + } + + private void someOuterMethod() { + + Transaction txn = server.beginTransaction(TxScope.required()); + try { + + server.find(Country.class).findRowCount(); + + try { + someInnerMethodWithFailure(); + + server.find(Product.class).findRowCount(); + txn.commit(); + + } catch (RuntimeException e) { + logger.info("Inner method failed with " + e); + } + + } finally { + // For REQUIRED ... the transaction can already been + // rolled back (by inner method) so that case is expected + // (and that is the case here - txn is already rolled back) + txn.end(); + } + + } + + private void someInnerMethodWithFailure() { + + logger.debug("someInnerMethod() ..."); + Transaction txn = server.beginTransaction(TxScope.required()); + try { + + server.find(Customer.class).findRowCount(); + + EBasic basic = new EBasic(); + basic.setName("ignore"); + server.save(basic); + + if (server != null) { + throw new RuntimeException("barf"); + } + + txn.commit(); + + } finally { + txn.end(); + logger.debug("someInnerMethod() ... done"); + } + } +} diff --git a/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNew.java b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNew.java new file mode 100644 index 000000000..11a231d09 --- /dev/null +++ b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNew.java @@ -0,0 +1,57 @@ +package com.avaje.tests.transaction; + +import com.avaje.ebean.*; +import com.avaje.tests.model.basic.Country; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.Product; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestNestedBeginRequiresNew extends BaseTestCase { + + Logger logger = LoggerFactory.getLogger(TestNestedBeginRequiresNew.class); + + EbeanServer server = Ebean.getServer(null); + + @Test + public void test() { + + someOuterMethod(); + } + + private void someOuterMethod() { + + Transaction txn = server.beginTransaction(TxScope.requiresNew()); + try { + + server.find(Country.class).findRowCount(); + + someInnerMethod(); + + server.find(Product.class).findRowCount(); + + txn.commit(); + + } finally { + txn.end(); + } + + } + + private void someInnerMethod() { + + logger.debug("someInnerMethod() ..."); + Transaction txn = server.beginTransaction(TxScope.requiresNew()); + try { + + server.find(Customer.class).findRowCount(); + txn.commit(); + + } finally { + txn.end(); + } + logger.debug("someInnerMethod() ... done"); + + } +} diff --git a/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNewWithFailure.java b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNewWithFailure.java new file mode 100644 index 000000000..eea7e3175 --- /dev/null +++ b/src/test/java/com/avaje/tests/transaction/TestNestedBeginRequiresNewWithFailure.java @@ -0,0 +1,63 @@ +package com.avaje.tests.transaction; + +import com.avaje.ebean.*; +import com.avaje.tests.model.basic.Country; +import com.avaje.tests.model.basic.Customer; +import com.avaje.tests.model.basic.Product; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestNestedBeginRequiresNewWithFailure extends BaseTestCase { + + Logger logger = LoggerFactory.getLogger(TestNestedBeginRequiresNewWithFailure.class); + + EbeanServer server = Ebean.getServer(null); + + @Test + public void test() { + + someOuterMethod(); + } + + private void someOuterMethod() { + + Transaction txn = server.beginTransaction(TxScope.requiresNew()); + try { + + server.find(Country.class).findRowCount(); + + try { + someInnerMethodWithFailure(); + } catch (RuntimeException e) { + logger.info("Inner method failed with " + e.getMessage()); + } + server.find(Product.class).findRowCount(); + + txn.commit(); + + } finally { + txn.end(); + } + + } + + private void someInnerMethodWithFailure() { + + logger.debug("someInnerMethod() ..."); + Transaction txn = server.beginTransaction(TxScope.requiresNew()); + try { + + server.find(Customer.class).findRowCount(); + + if (server != null) { + throw new RuntimeException("barf"); + } + txn.commit(); + + } finally { + txn.end(); + } + logger.debug("someInnerMethod() ... done"); + } +}