diff --git a/src/main/java/io/ebeaninternal/api/ScopeTrans.java b/src/main/java/io/ebeaninternal/api/ScopeTrans.java index 641a8c639..d1326423b 100644 --- a/src/main/java/io/ebeaninternal/api/ScopeTrans.java +++ b/src/main/java/io/ebeaninternal/api/ScopeTrans.java @@ -89,6 +89,10 @@ public class ScopeTrans { } + public String toString() { + return "ScopeTrans[" + transaction + "]"; + } + /** * Return the current/active transaction. */ diff --git a/src/main/java/io/ebeaninternal/api/ScopedTransaction.java b/src/main/java/io/ebeaninternal/api/ScopedTransaction.java index 337f12595..9ecd6a3b7 100644 --- a/src/main/java/io/ebeaninternal/api/ScopedTransaction.java +++ b/src/main/java/io/ebeaninternal/api/ScopedTransaction.java @@ -25,6 +25,10 @@ public class ScopedTransaction extends SpiTransactionProxy { this.manager = manager; } + public String toString() { + return "ScopedTransaction[" + current + "]"; + } + /** * Push the scope transaction. */ @@ -56,6 +60,7 @@ public class ScopedTransaction extends SpiTransactionProxy { private void pop() { if (!stack.isEmpty()) { current = stack.pop(); + transaction = current.getTransaction(); } else { manager.set(null); } diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 842a5c468..c36f7195f 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -724,33 +724,31 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { * This will also potentially throw exceptions for MANDATORY and NEVER types. *

*/ - private boolean createNewTransaction(SpiTransaction t, TxScope scope) { - - TxType type = scope.getType(); + private boolean createNewTransaction(SpiTransaction current, TxType type) { switch (type) { case REQUIRED: - return t == null; + return current == null; case REQUIRES_NEW: return true; case MANDATORY: - if (t == null) { + if (current == null) { throw new PersistenceException("Transaction missing when MANDATORY"); } return false; + case SUPPORTS: + return current == null; + case NEVER: - if (t != null) { + if (current != null) { throw new PersistenceException("Transaction exists for Transactional NEVER"); } - return false; - - case SUPPORTS: - return false; + return true; // always use NoTransaction instance case NOT_SUPPORTED: - throw new RuntimeException("NOT_SUPPORTED should already be handled?"); + return true; // always use NoTransaction instance default: throw new RuntimeException("Should never get here?"); @@ -811,14 +809,17 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { SpiTransaction transaction = txnContainer.current(); - boolean createTransaction; - if (txScope.getType() == TxType.NOT_SUPPORTED) { - createTransaction = false; - transaction = null; - } else { - createTransaction = createNewTransaction(transaction, txScope); - if (createTransaction) { - transaction = transactionManager.createTransaction(txScope.getProfileId(), true, txScope.getIsolationLevel()); + TxType type = txScope.getType(); + boolean createTransaction = createNewTransaction(transaction, type); + if (createTransaction) { + switch (type) { + case SUPPORTS: + case NOT_SUPPORTED: + case NEVER: + transaction = NoTransaction.INSTANCE; + break; + default: + transaction = transactionManager.createTransaction(txScope.getProfileId(), true, txScope.getIsolationLevel()); } } diff --git a/src/main/java/io/ebeaninternal/server/core/NoTransaction.java b/src/main/java/io/ebeaninternal/server/core/NoTransaction.java new file mode 100644 index 000000000..58a2574d4 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/core/NoTransaction.java @@ -0,0 +1,419 @@ +package io.ebeaninternal.server.core; + +import io.ebean.TransactionCallback; +import io.ebean.annotation.DocStoreMode; +import io.ebean.annotation.PersistBatch; +import io.ebean.bean.PersistenceContext; +import io.ebean.event.changelog.BeanChange; +import io.ebean.event.changelog.ChangeSet; +import io.ebeaninternal.api.SpiProfileTransactionEvent; +import io.ebeaninternal.api.SpiTransaction; +import io.ebeaninternal.api.TransactionEvent; +import io.ebeaninternal.server.persist.BatchControl; +import io.ebeaninternal.server.transaction.ProfileStream; +import io.ebeanservice.docstore.api.DocStoreTransaction; + +import javax.persistence.PersistenceException; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Place holder for use with SUPPORTS and NEVER transactional when there really isn't a transaction. + */ +class NoTransaction implements SpiTransaction { + + static final NoTransaction INSTANCE = new NoTransaction(); + + @Override + public boolean isActive() { + // always false + return false; + } + + @Override + public void commitAndContinue() { + // do nothing + } + + @Override + public void commit() { + // do nothing + } + + @Override + public void rollback() throws PersistenceException { + // do nothing + } + + + @Override + public void rollback(Throwable e) throws PersistenceException { + // do nothing + } + + @Override + public void end() { + // do nothing + } + + @Override + public void close() { + // do nothing + } + + @Override + public String getLogPrefix() { + return null; + } + + @Override + public boolean isLogSql() { + return false; + } + + @Override + public boolean isLogSummary() { + return false; + } + + @Override + public void logSql(String msg) { + + } + + @Override + public void logSummary(String msg) { + + } + + @Override + public void registerDeferred(PersistDeferredRelationship derived) { + + } + + @Override + public void registerDeleteBean(Integer hash) { + + } + + @Override + public void unregisterDeleteBean(Integer hash) { + + } + + @Override + public boolean isRegisteredDeleteBean(Integer hash) { + return false; + } + + @Override + public void unregisterBean(Object bean) { + + } + + @Override + public boolean isRegisteredBean(Object bean) { + return false; + } + + @Override + public String getId() { + return null; + } + + @Override + public Boolean isUpdateAllLoadedProperties() { + return null; + } + + @Override + public DocStoreMode getDocStoreMode() { + return null; + } + + @Override + public int getDocStoreBatchSize() { + return 0; + } + + @Override + public void register(TransactionCallback callback) { + + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public void setReadOnly(boolean readOnly) { + + } + + @Override + public void setRollbackOnly() { + + } + + @Override + public boolean isRollbackOnly() { + return false; + } + + + @Override + public void setDocStoreMode(DocStoreMode mode) { + + } + + @Override + public void setDocStoreBatchSize(int batchSize) { + + } + + @Override + public void setPersistCascade(boolean persistCascade) { + + } + + @Override + public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) { + + } + + @Override + public void setSkipCache(boolean skipCache) { + + } + + @Override + public boolean isSkipCache() { + return false; + } + + @Override + public void setBatchMode(boolean useBatch) { + + } + + @Override + public void setBatch(PersistBatch persistBatchMode) { + + } + + @Override + public PersistBatch getBatch() { + return null; + } + + @Override + public void setBatchOnCascade(PersistBatch batchOnCascadeMode) { + + } + + @Override + public PersistBatch getBatchOnCascade() { + return null; + } + + @Override + public void setBatchSize(int batchSize) { + + } + + @Override + public int getBatchSize() { + return 0; + } + + @Override + public void setBatchGetGeneratedKeys(boolean getGeneratedKeys) { + + } + + @Override + public void setBatchFlushOnMixed(boolean batchFlushOnMixed) { + + } + + @Override + public void setBatchFlushOnQuery(boolean batchFlushOnQuery) { + + } + + @Override + public boolean isBatchFlushOnQuery() { + return false; + } + + @Override + public void flush() throws PersistenceException { + + } + + @Override + public void flushBatch() throws PersistenceException { + + } + + @Override + public Connection getConnection() { + return null; + } + + @Override + public void addModification(String tableName, boolean inserts, boolean updates, boolean deletes) { + + } + + @Override + public void putUserObject(String name, Object value) { + + } + + @Override + public Object getUserObject(String name) { + return null; + } + + @Override + public Boolean getBatchGetGeneratedKeys() { + return null; + } + + @Override + public void depth(int diff) { + + } + + @Override + public int depth() { + return 0; + } + + @Override + public boolean isExplicit() { + return false; + } + + @Override + public TransactionEvent getEvent() { + return null; + } + + @Override + public boolean isPersistCascade() { + return false; + } + + @Override + public boolean isBatchThisRequest(PersistRequest.Type type) { + return false; + } + + @Override + public BatchControl getBatchControl() { + return null; + } + + @Override + public void setBatchControl(BatchControl control) { + + } + + @Override + public PersistenceContext getPersistenceContext() { + return null; + } + + @Override + public void setPersistenceContext(PersistenceContext context) { + + } + + @Override + public Connection getInternalConnection() { + return null; + } + + @Override + public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName) { + return false; + } + + @Override + public boolean checkBatchEscalationOnCascade(PersistRequestBean request) { + return false; + } + + @Override + public void flushBatchOnCascade() { + + } + + @Override + public void flushBatchOnRollback() { + + } + + @Override + public PersistenceException translate(String message, SQLException cause) { + return null; + } + + @Override + public void markNotQueryOnly() { + + } + + @Override + public void checkBatchEscalationOnCollection() { + + } + + @Override + public void flushBatchOnCollection() { + + } + + @Override + public void addBeanChange(BeanChange beanChange) { + + } + + @Override + public void sendChangeLog(ChangeSet changeSet) { + + } + + @Override + public DocStoreTransaction getDocStoreTransaction() { + return null; + } + + @Override + public void setTenantId(Object tenantId) { + + } + + @Override + public Object getTenantId() { + return null; + } + + @Override + public long profileOffset() { + return 0; + } + + @Override + public void profileEvent(SpiProfileTransactionEvent event) { + + } + + @Override + public ProfileStream profileStream() { + return null; + } +} diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionMap.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionMap.java index 7a4f473e0..033e6c74a 100644 --- a/src/main/java/io/ebeaninternal/server/transaction/TransactionMap.java +++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionMap.java @@ -39,8 +39,7 @@ public class TransactionMap { */ public State getStateWithCreate(String serverName) { - State state = map.computeIfAbsent(serverName, k -> new State()); - return state; + return map.computeIfAbsent(serverName, k -> new State()); } /** diff --git a/src/test/java/org/tests/transaction/TestTransactionalNever.java b/src/test/java/org/tests/transaction/TestTransactionalNever.java new file mode 100644 index 000000000..e62aaae19 --- /dev/null +++ b/src/test/java/org/tests/transaction/TestTransactionalNever.java @@ -0,0 +1,47 @@ +package org.tests.transaction; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Transaction; +import io.ebean.annotation.Transactional; +import io.ebean.annotation.TxType; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.PersistenceException; + +public class TestTransactionalNever extends BaseTestCase { + + private static final Logger log = LoggerFactory.getLogger(TestTransactionalNever.class); + + private Transaction outerTxn; + + @Test + public void testWithNever() { + + new SomeTransactionalWithNever().doStuff(); + } + + @Test(expected = PersistenceException.class) + public void testBarfOnExisting() { + doWithTransaction(); + } + + class SomeTransactionalWithNever { + + @Transactional(type = TxType.NEVER) + void doStuff() { + outerTxn = Ebean.currentTransaction(); + log.info("currentTransaction ...{}", outerTxn); + } + } + + @Transactional + private void doWithTransaction() { + + // always barf + new SomeTransactionalWithNever().doStuff(); + } + +} diff --git a/src/test/java/org/tests/transaction/TestTransactionalRequired.java b/src/test/java/org/tests/transaction/TestTransactionalRequired.java new file mode 100644 index 000000000..be62f7d66 --- /dev/null +++ b/src/test/java/org/tests/transaction/TestTransactionalRequired.java @@ -0,0 +1,88 @@ +package org.tests.transaction; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Transaction; +import io.ebean.annotation.Transactional; +import io.ebean.annotation.TxType; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class TestTransactionalRequired extends BaseTestCase { + + private static final Logger log = LoggerFactory.getLogger(TestTransactionalRequired.class); + + private Transaction outerTxn; + private Transaction innerTxn; + private Transaction currentTxn; + + @Test + public void noCurrentTransaction() { + + outerTxn = null; + + assertNull(Ebean.currentTransaction()); + + new OuterTransactionalWithRequired().doOuter(); + + assertNull(Ebean.currentTransaction()); + + assertNotNull(outerTxn); + assertNotNull(innerTxn); + assertSame(outerTxn, innerTxn); + } + + + @Test + public void withOuterBegin() { + + outerTxn = null; + Ebean.beginTransaction(); + try { + currentTxn = Ebean.currentTransaction(); + assertNotNull(currentTxn); + new OuterTransactionalWithRequired().doOuter(); + + // the original transaction was restored + Transaction restored = Ebean.currentTransaction(); + assertSame(currentTxn, restored); + assertSame(currentTxn, innerTxn); + assertSame(currentTxn, outerTxn); + } finally { + Ebean.endTransaction(); + } + + assertNull(Ebean.currentTransaction()); + } + + class OuterTransactionalWithRequired { + + @Transactional(type = TxType.REQUIRED) + void doOuter() { + outerTxn = Ebean.currentTransaction(); + log.info("outer ...{}", outerTxn); + + new InTransactionalWithRequired().doInner(); + + Transaction current = Ebean.currentTransaction(); + assertSame(outerTxn, current); + } + + } + + class InTransactionalWithRequired { + + @Transactional(type = TxType.REQUIRED) + void doInner() { + innerTxn = Ebean.currentTransaction(); + log.info("inner ...{}", innerTxn); + } + } + + +} diff --git a/src/test/java/org/tests/transaction/TestTransactionalRequiresNew.java b/src/test/java/org/tests/transaction/TestTransactionalRequiresNew.java new file mode 100644 index 000000000..03e353f39 --- /dev/null +++ b/src/test/java/org/tests/transaction/TestTransactionalRequiresNew.java @@ -0,0 +1,70 @@ +package org.tests.transaction; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Transaction; +import io.ebean.annotation.Transactional; +import io.ebean.annotation.TxType; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class TestTransactionalRequiresNew extends BaseTestCase { + + private static final Logger log = LoggerFactory.getLogger(TestTransactionalRequiresNew.class); + + private Connection outerConn; + private Transaction outerTxn; + + @Test + public void basic() { + + outerTxn = null; + assertNull(Ebean.currentTransaction()); + + new OuterTransactionalWithRequired().doOuter(); + + assertNull(Ebean.currentTransaction()); + assertNotNull(outerTxn); + } + + + class OuterTransactionalWithRequired { + + @Transactional + void doOuter() { + outerTxn = Ebean.currentTransaction(); + log.info("outer before ...{}", outerTxn); + outerConn = outerTxn.getConnection(); + + new InTransactionalWithRequiresNew().doInner(); + + // restore the outerTxn + Transaction current = Ebean.currentTransaction(); + log.info("outer after ...{}", current); + assertSame(outerConn, current.getConnection()); + } + + } + + class InTransactionalWithRequiresNew { + + @Transactional(type = TxType.REQUIRES_NEW) + void doInner() { + Transaction innerTxn = Ebean.currentTransaction(); + log.info("inner ...{} {}", innerTxn); + + Connection connection = innerTxn.getConnection(); + assertNotSame(connection, outerConn); + } + } + + +} diff --git a/src/test/java/org/tests/transaction/TestTransactionalSupports.java b/src/test/java/org/tests/transaction/TestTransactionalSupports.java new file mode 100644 index 000000000..8e1a974c1 --- /dev/null +++ b/src/test/java/org/tests/transaction/TestTransactionalSupports.java @@ -0,0 +1,53 @@ +package org.tests.transaction; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.Transaction; +import io.ebean.annotation.Transactional; +import io.ebean.annotation.TxType; +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 TestTransactionalSupports extends BaseTestCase { + + private static final Logger log = LoggerFactory.getLogger(TestTransactionalSupports.class); + + private Transaction outerTxn; + + @Test + public void noCurrentTransaction() { + + outerTxn = null; + new SomeTransactionalWithSupports().doStuff(); + assertNull(outerTxn); + } + + + @Test + public void withOuterTransaction_expect_currentTransactionAvailable() { + + outerTxn = null; + Ebean.beginTransaction(); + try { + new SomeTransactionalWithSupports().doStuff(); + assertNotNull(outerTxn); + } finally { + Ebean.endTransaction(); + } + } + + class SomeTransactionalWithSupports { + + @Transactional(type = TxType.SUPPORTS) + void doStuff() { + outerTxn = Ebean.currentTransaction(); + log.info("outer ...{}", outerTxn); + } + } + + +}