diff --git a/pom.xml b/pom.xml
index ac993c675..54321c66a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
io.ebean
ebean
- 11.9.2-SNAPSHOT
+ 11.10.1-SNAPSHOT
jar
ebean
diff --git a/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java b/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java
index a1a5c812e..423c37792 100644
--- a/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java
+++ b/src/main/java/io/ebeaninternal/api/SpiEbeanServer.java
@@ -80,6 +80,11 @@ public interface SpiEbeanServer extends EbeanServer, BeanLoader, BeanCollectionL
*/
void clearQueryStatistics();
+ /**
+ * Return the transaction manager.
+ */
+ SpiTransactionManager getTransactionManager();
+
/**
* Return all the descriptors.
*/
diff --git a/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java b/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
new file mode 100644
index 000000000..e914d200e
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java
@@ -0,0 +1,37 @@
+package io.ebeaninternal.api;
+
+import io.ebean.TxScope;
+
+import javax.sql.DataSource;
+
+/**
+ * Service provider interface for the transaction manager.
+ */
+public interface SpiTransactionManager {
+
+ /**
+ * Return the main DataSource.
+ */
+ DataSource getDataSource();
+
+ /**
+ * Return the read only DataSource (if defined).
+ */
+ DataSource getReadOnlyDataSource();
+
+ /**
+ * Return the currently active transaction (can be null).
+ */
+ SpiTransaction get();
+
+ /**
+ * Push an externally managed transaction into scope (e.g. Spring managed transaction).
+ */
+ ScopedTransaction externalBeginTransaction(SpiTransaction transaction, TxScope txScope);
+
+ /**
+ * Called when an externally managed transaction has completed.
+ */
+ void externalRemoveTransaction();
+
+}
diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
index bd5c42eab..b9267a91b 100644
--- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
+++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
@@ -61,6 +61,7 @@ import io.ebeaninternal.api.SpiJsonContext;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.SpiQuery.Type;
import io.ebeaninternal.api.SpiTransaction;
+import io.ebeaninternal.api.SpiTransactionManager;
import io.ebeaninternal.api.TransactionEventTable;
import io.ebeaninternal.dbmigration.DdlGenerator;
import io.ebeaninternal.dbmigration.ddlgeneration.DdlHandler;
@@ -1879,6 +1880,14 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
return beanDescriptorManager.getBeanDescriptorList();
}
+ /**
+ * Return the transaction manager.
+ */
+ @Override
+ public SpiTransactionManager getTransactionManager() {
+ return transactionManager;
+ }
+
public void register(BeanPersistController c) {
List> list = beanDescriptorManager.getBeanDescriptorList();
for (BeanDescriptor> aList : list) {
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
index 54d847089..3b880777a 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
@@ -16,6 +16,7 @@ import io.ebeaninternal.api.ScopeTrans;
import io.ebeaninternal.api.ScopedTransaction;
import io.ebeaninternal.api.SpiProfileHandler;
import io.ebeaninternal.api.SpiTransaction;
+import io.ebeaninternal.api.SpiTransactionManager;
import io.ebeaninternal.api.TransactionEvent;
import io.ebeaninternal.api.TransactionEventTable;
import io.ebeaninternal.api.TransactionEventTable.TableIUD;
@@ -46,7 +47,7 @@ import java.util.Set;
* Keeps the Cache and Cluster in synch when transactions are committed.
*
*/
-public class TransactionManager {
+public class TransactionManager implements SpiTransactionManager {
private static final Logger logger = LoggerFactory.getLogger(TransactionManager.class);
@@ -558,6 +559,23 @@ public class TransactionManager {
}
}
+ @Override
+ public void externalRemoveTransaction() {
+ scopeManager.replace(null);
+ }
+
+ /**
+ * Push an externally created transaction into scope. This transaction is usually managed externally
+ * (e.g. Spring managed transaction).
+ */
+ @Override
+ public ScopedTransaction externalBeginTransaction(SpiTransaction transaction, TxScope txScope) {
+ ScopedTransaction scopedTxn = new ScopedTransaction(scopeManager);
+ scopedTxn.push(new ScopeTrans(rollbackOnChecked, false, transaction, txScope));
+ scopeManager.set(scopedTxn);
+ return scopedTxn;
+ }
+
/**
* Begin a scoped transaction.
*/
diff --git a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java
index a880c246e..cd689fd65 100644
--- a/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java
+++ b/src/test/java/io/ebeaninternal/api/TDSpiEbeanServer.java
@@ -1,6 +1,29 @@
package io.ebeaninternal.api;
-import io.ebean.*;
+import io.ebean.AutoTune;
+import io.ebean.BackgroundExecutor;
+import io.ebean.BeanState;
+import io.ebean.CallableSql;
+import io.ebean.DocumentStore;
+import io.ebean.ExpressionFactory;
+import io.ebean.Filter;
+import io.ebean.FutureIds;
+import io.ebean.FutureList;
+import io.ebean.FutureRowCount;
+import io.ebean.PagedList;
+import io.ebean.PersistenceContextScope;
+import io.ebean.Query;
+import io.ebean.QueryIterator;
+import io.ebean.SqlQuery;
+import io.ebean.SqlRow;
+import io.ebean.SqlUpdate;
+import io.ebean.Transaction;
+import io.ebean.TransactionCallback;
+import io.ebean.TxScope;
+import io.ebean.Update;
+import io.ebean.UpdateQuery;
+import io.ebean.ValuePair;
+import io.ebean.Version;
import io.ebean.annotation.TxIsolation;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.CallStack;
@@ -138,6 +161,11 @@ public class TDSpiEbeanServer implements SpiEbeanServer {
return null;
}
+ @Override
+ public SpiTransactionManager getTransactionManager() {
+ return null;
+ }
+
@Override
public List> getBeanDescriptors() {
return null;
diff --git a/src/test/java/io/ebeaninternal/server/transaction/TransactionManagerTest.java b/src/test/java/io/ebeaninternal/server/transaction/TransactionManagerTest.java
new file mode 100644
index 000000000..6b851c56d
--- /dev/null
+++ b/src/test/java/io/ebeaninternal/server/transaction/TransactionManagerTest.java
@@ -0,0 +1,78 @@
+package io.ebeaninternal.server.transaction;
+
+import io.ebean.BaseTestCase;
+import io.ebean.Ebean;
+import io.ebean.Transaction;
+import io.ebean.TxScope;
+import io.ebeaninternal.api.ScopedTransaction;
+import io.ebeaninternal.api.SpiEbeanServer;
+import io.ebeaninternal.api.SpiTransaction;
+import io.ebeaninternal.api.SpiTransactionManager;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.tests.model.basic.Customer;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TransactionManagerTest extends BaseTestCase {
+
+ private static final Logger log = LoggerFactory.getLogger(TransactionManagerTest.class);
+
+ @Test
+ public void beginExternalTransaction() throws SQLException {
+
+ SpiEbeanServer server = spiEbeanServer();
+
+ SpiTransactionManager transactionManager = server.getTransactionManager();
+
+ DataSource dataSource = transactionManager.getDataSource();
+ Connection connection = dataSource.getConnection();
+
+ SpiTransaction externalTxn = new ExternalJdbcTransaction("external0", true, connection, null);
+
+ // push an externally managed transaction onto scope
+ ScopedTransaction scopedTransaction = transactionManager.externalBeginTransaction(externalTxn, TxScope.required());
+
+ Transaction current = Transaction.current();
+ assertThat(current).as("external transaction is in scope").isSameAs(scopedTransaction);
+
+ Customer.find.byName("In external");
+
+ log.info("inner begin");
+ Transaction inner = Ebean.beginTransaction();
+ try {
+
+ current = Transaction.current();
+ assertThat(current).as("still using external transaction, nested transaction pushed").isSameAs(scopedTransaction);
+
+ Customer.find.byName("In inner");
+ log.info("inner commit");
+ inner.commit();
+ } finally {
+ log.info("inner end");
+ inner.end();
+ }
+
+ current = Transaction.current();
+ assertThat(current).as("external transaction still in scope").isSameAs(scopedTransaction);
+
+ Customer.find.byName("external still active");
+
+
+ log.info("external transaction ends with commit and remove");
+ connection.commit();
+ // remove externally managed transaction out of scope
+ transactionManager.externalRemoveTransaction();
+
+
+ current = Transaction.current();
+ assertThat(current).as("external transaction out of scope").isNull();
+
+ Customer.find.byNameStatus("New implicit transaction", Customer.Status.ACTIVE);
+ }
+}