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)}. + *
+ * + *{@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();
+ * }
+ *
+ * }
+ *
+ * {@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)}. + *
+ * + *{@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();
+ * }
+ *
+ * }
+ *
+ * {@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 Listdiff --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"); + } +}