From 16be213faff696b3a4ce564a674cf18edecbb4db Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Fri, 5 Aug 2016 10:39:17 +1200 Subject: [PATCH] #808 - ENH: Add Transaction commitAndContinue() ... --- .../java/com/avaje/ebean/Transaction.java | 44 ++++- .../ebeaninternal/api/ScopedTransaction.java | 5 + .../server/transaction/JdbcTransaction.java | 53 ++++- .../java/com/avaje/ebean/BaseTestCase.java | 2 +- .../java/com/avaje/tests/model/m2m/MnyB.java | 7 + .../transaction/TestCommitAndContinue.java | 183 ++++++++++++++++++ 6 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/avaje/tests/transaction/TestCommitAndContinue.java 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> + * + */ + 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> + * */ void commit() throws RollbackException; /** * Rollback the transaction. + *

+ * This performs rollback, closes underlying resources and marks the transaction as "In active". + *

+ *

Functions/h3> + * */ 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(); + } + +}