diff --git a/src/main/java/com/avaje/ebean/Transaction.java b/src/main/java/com/avaje/ebean/Transaction.java
index 526fe28bb..560a7278b 100644
--- a/src/main/java/com/avaje/ebean/Transaction.java
+++ b/src/main/java/com/avaje/ebean/Transaction.java
@@ -54,13 +54,52 @@ public interface Transaction extends Closeable {
*/
void setReadOnly(boolean readOnly);
+ /**
+ * Commits the transaction at this point with the expectation that another
+ * commit (or rollback or end) will occur later to complete the transaction.
+ *
+ * This is similar to commit() but leaves the transaction "Active".
+ *
+ * Functions/h3>
+ *
+ * - Flush the JDBC batch buffer
+ * - Call commit on the underlying JDBC connection
+ * - Trigger any registered TransactionCallbacks
+ * - Perform post-commit processing updating L2 cache, ElasticSearch etc
+ *
+ */
+ void commitAndContinue() throws RollbackException;
+
/**
* Commit the transaction.
+ *
+ * This performs commit and completes the transaction closing underlying resources and
+ * marking the transaction as "In active".
+ *
+ * Functions/h3>
+ *
+ * - Flush the JDBC batch buffer
+ * - Call commit on the underlying JDBC connection
+ * - Trigger any registered TransactionCallbacks
+ * - Perform post-commit processing updating L2 cache, ElasticSearch etc
+ * - Close any underlying resources, closing the underlying JDBC connection
+ * - Mark the transaction as "Inactive"
+ *
*/
void commit() throws RollbackException;
/**
* Rollback the transaction.
+ *
+ * This performs rollback, closes underlying resources and marks the transaction as "In active".
+ *
+ * Functions/h3>
+ *
+ * - Call rollback on the underlying JDBC connection
+ * - Trigger any registered TransactionCallbacks
+ * - Close any underlying resources, closing the underlying JDBC connection
+ * - Mark the transaction as "Inactive"
+ *
*/
void rollback() throws PersistenceException;
@@ -422,9 +461,8 @@ public interface Transaction extends Closeable {
/**
* Add an arbitrary user object to the transaction. The objects added have no
- * impact on any internals of ebena and are solely meant as a convenient
- * method push user information to e.g. the
- * {@link com.avaje.ebean.event.TransactionEventListener}.
+ * impact on any internals of ebean and are solely meant as a convenient
+ * method push user information (although somewhat replaced by TransactionCallback).
*/
void putUserObject(String name, Object value);
diff --git a/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java b/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java
index 6a5354061..0d29f3aaf 100644
--- a/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java
+++ b/src/main/java/com/avaje/ebeaninternal/api/ScopedTransaction.java
@@ -32,6 +32,11 @@ public class ScopedTransaction implements SpiTransaction {
this.transaction = scopeTrans.getTransaction();
}
+ @Override
+ public void commitAndContinue() throws RollbackException {
+ transaction.commitAndContinue();
+ }
+
@Override
public void commit() throws RollbackException {
scopeTrans.commitTransaction();
diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java
index adb8433ba..80f2ef1d9 100644
--- a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java
+++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java
@@ -882,6 +882,48 @@ public class JdbcTransaction implements SpiTransaction {
connection.commit();
}
+ /**
+ * Batch flush, jdbc commit, trigger registered TransactionCallbacks, notify l2 cache etc.
+ */
+ private void flushCommitAndNotify() throws SQLException {
+ if (batchControl != null && !batchControl.isEmpty()) {
+ batchControl.flush();
+ }
+ firePreCommit();
+ // only performCommit can throw an exception
+ performCommit();
+ firePostCommit();
+ notifyCommit();
+ }
+
+ /**
+ * Perform a commit, fire callbacks and notify l2 cache etc.
+ *
+ * This leaves the transaction active and expects another commit
+ * to occur later (which closes the underlying connection etc).
+ *
+ */
+ @Override
+ public void commitAndContinue() throws RollbackException {
+ if (rollbackOnly) {
+ return;
+ }
+ if (!isActive()) {
+ throw new IllegalStateException(illegalStateMessage);
+ }
+ try {
+ flushCommitAndNotify();
+ // the event has been sent to the transaction manager
+ // for postCommit processing (l2 cache updates etc)
+ // start a new transaction event
+ event = new TransactionEvent();
+
+ } catch (Exception e) {
+ doRollback(e);
+ throw new RollbackException(e);
+ }
+ }
+
/**
* Commit the transaction.
*/
@@ -894,20 +936,11 @@ public class JdbcTransaction implements SpiTransaction {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
-
try {
if (queryOnly) {
connectionEndForQueryOnly();
-
} else {
- if (batchControl != null && !batchControl.isEmpty()) {
- batchControl.flush();
- }
- firePreCommit();
- // only performCommit can throw an exception
- performCommit();
- firePostCommit();
- notifyCommit();
+ flushCommitAndNotify();
}
} catch (Exception e) {
diff --git a/src/test/java/com/avaje/ebean/BaseTestCase.java b/src/test/java/com/avaje/ebean/BaseTestCase.java
index e6be4834e..614a8824e 100644
--- a/src/test/java/com/avaje/ebean/BaseTestCase.java
+++ b/src/test/java/com/avaje/ebean/BaseTestCase.java
@@ -13,7 +13,7 @@ public class BaseTestCase {
static {
logger.debug("... preStart");
- if (!AgentLoader.loadAgentFromClasspath("avaje-ebeanorm-agent","debug=1;packages=com.avaje.tests,org.avaje.test")) {
+ if (!AgentLoader.loadAgentFromClasspath("ebean-agent","debug=1;packages=com.avaje.tests,org.avaje.test")) {
logger.info("avaje-ebeanorm-agent not found in classpath - not dynamically loaded");
}
}
diff --git a/src/test/java/com/avaje/tests/model/m2m/MnyB.java b/src/test/java/com/avaje/tests/model/m2m/MnyB.java
index 6009cb81d..371d2f994 100644
--- a/src/test/java/com/avaje/tests/model/m2m/MnyB.java
+++ b/src/test/java/com/avaje/tests/model/m2m/MnyB.java
@@ -19,6 +19,13 @@ public class MnyB extends BaseModel {
@ManyToMany(cascade = CascadeType.REMOVE)
List cs;
+ public MnyB(String name) {
+ this.name = name;
+ }
+
+ public MnyB() {
+ }
+
public String getName() {
return name;
}
diff --git a/src/test/java/com/avaje/tests/transaction/TestCommitAndContinue.java b/src/test/java/com/avaje/tests/transaction/TestCommitAndContinue.java
new file mode 100644
index 000000000..74f7e5b94
--- /dev/null
+++ b/src/test/java/com/avaje/tests/transaction/TestCommitAndContinue.java
@@ -0,0 +1,183 @@
+package com.avaje.tests.transaction;
+
+import com.avaje.ebean.BaseTestCase;
+import com.avaje.ebean.Ebean;
+import com.avaje.ebean.EbeanServer;
+import com.avaje.ebean.Transaction;
+import com.avaje.ebean.annotation.Transactional;
+import com.avaje.tests.model.m2m.MnyB;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class TestCommitAndContinue extends BaseTestCase {
+
+ private static final Logger logger = LoggerFactory.getLogger("org.avaje.ebean.TXN");
+
+ @Test
+ @Transactional
+ public void transactional_partialSuccess() {
+
+ MnyB a = new MnyB("a100");
+ MnyB b = new MnyB("b200");
+
+ a.save();
+
+ // commit at this point
+ Ebean.currentTransaction().commitAndContinue();
+
+ try {
+ b.save();
+
+ // some error occurs
+ throw new IllegalStateException();
+
+ } catch (IllegalStateException e) {
+ // mark the transaction as rollback
+ Ebean.currentTransaction().setRollbackOnly();
+
+ // use a different transaction to assert
+ EbeanServer server = Ebean.getDefaultServer();
+ Transaction anotherTxn = server.createTransaction();
+
+ // success prior to commitAndContinue
+ assertNotNull(server.find(MnyB.class, a.getId(), anotherTxn));
+
+ // insert failed after commitAndContinue
+ assertNull(server.find(MnyB.class, b.getId(), anotherTxn));
+ }
+ }
+
+ /**
+ * The @Transactional is nicer to me.
+ */
+ @Test
+ public void tryFinally_partialSuccess() {
+
+ MnyB a = new MnyB("a100");
+ MnyB b = new MnyB("b200");
+
+ EbeanServer server = Ebean.getDefaultServer();
+ Transaction txn = server.beginTransaction();
+ try {
+ a.save();
+ // commit at this point
+ txn.commitAndContinue();
+
+ try {
+ b.save();
+
+ // some error occurs
+ throw new IllegalStateException();
+
+ } catch (IllegalStateException e) {
+ // mark the transaction as rollback
+ txn.setRollbackOnly();
+
+ // use a different transaction to assert
+ Transaction anotherTxn = server.createTransaction();
+ // success prior to commitAndContinue
+ assertNotNull(server.find(MnyB.class, a.getId(), anotherTxn));
+ // insert failed after commitAndContinue
+ assertNull(server.find(MnyB.class, b.getId(), anotherTxn));
+ }
+
+ // does not commit due to the txn.setRollbackOnly();
+ txn.commit();
+
+ } finally {
+ server.endTransaction();
+ }
+ }
+
+ @Test
+ @Transactional
+ public void transactional_partialSuccess_secondTransactionInsert() {
+
+ MnyB a = new MnyB("a100");
+ MnyB b = new MnyB("b200");
+ MnyB c = new MnyB("c300");
+
+ a.save();
+
+ // commit at this point
+ Ebean.currentTransaction().commitAndContinue();
+
+ try {
+ b.save();
+
+ // some error occurs
+ throw new IllegalStateException();
+
+ } catch (IllegalStateException e) {
+ // mark the transaction as rollback
+ Ebean.currentTransaction().setRollbackOnly();
+
+ // use a different transaction to do something useful
+ EbeanServer server = Ebean.getDefaultServer();
+ Transaction txn2 = server.createTransaction();
+ try {
+ server.save(c, txn2);
+ txn2.commit();
+ } finally {
+ txn2.end();
+ }
+ }
+
+ // asserts
+
+ EbeanServer server = Ebean.getDefaultServer();
+ Transaction txnForAssert = server.createTransaction();
+
+ // success prior to commitAndContinue
+ assertNotNull(server.find(MnyB.class, a.getId(), txnForAssert));
+
+ // insert failed after commitAndContinue
+ assertNull(server.find(MnyB.class, b.getId(), txnForAssert));
+
+ // successful insert using txn2
+ assertNotNull(server.find(MnyB.class, c.getId(), txnForAssert));
+ }
+
+ @Test
+ public void basic() {
+
+ MnyB a = new MnyB("a");
+ MnyB b = new MnyB("b");
+ MnyB c = new MnyB("c");
+
+ Transaction txn = Ebean.beginTransaction();
+ try {
+ a.save();
+ txn.commitAndContinue();
+
+ txn.setBatchMode(true);
+ b.save();
+ logger.info("... pre commitAndContinue");
+ txn.commitAndContinue();
+
+ c.save();
+ txn.commit();
+
+ } finally {
+ txn.end();
+ }
+ }
+
+ @Test
+ @Transactional
+ public void runTransactional() {
+
+ new MnyB("a100").save();
+ new MnyB("a101").save();
+
+ Ebean.currentTransaction().commitAndContinue();
+
+ new MnyB("a200").save();
+ new MnyB("a201").save();
+ }
+
+}