diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java
index d4918e84e..0cbfd6e3c 100644
--- a/ebean-api/src/main/java/io/ebean/Transaction.java
+++ b/ebean-api/src/main/java/io/ebean/Transaction.java
@@ -59,6 +59,14 @@ public interface Transaction extends AutoCloseable {
*/
void register(TransactionCallback callback);
+ /**
+ * EXPERIMENTAL - turn on transparent persistence and batchMode true.
+ *
+ * With this turned on beans that are dirty in the persistence context
+ * are automatically persisted on flush() and commit().
+ */
+ void setTransparentPersistence(boolean transparentPersistence);
+
/**
* Set a label on the transaction.
*
diff --git a/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java b/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java
index a8a42e51b..8c861af1f 100644
--- a/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java
+++ b/ebean-api/src/main/java/io/ebean/bean/PersistenceContext.java
@@ -1,5 +1,7 @@
package io.ebean.bean;
+import java.util.List;
+
/**
* Holds entity beans by there type and id.
*
@@ -77,6 +79,11 @@ public interface PersistenceContext {
*/
boolean resetLimit();
+ /**
+ * Return the list of dirty beans held by this persistence context.
+ */
+ List dirtyBeans();
+
/**
* Wrapper on a bean to also indicate if a bean has been deleted.
*
diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java
index f5a1db07b..6e95a114e 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/api/ScopedTransaction.java
@@ -131,6 +131,11 @@ public class ScopedTransaction extends SpiTransactionProxy {
}
}
+ @Override
+ public void setTransparentPersistence(boolean transparentPersistence) {
+ current.getTransaction().setTransparentPersistence(transparentPersistence);
+ }
+
@Override
public void setRollbackOnly() {
current.setRollbackOnly();
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
index 52b94bfb6..6595d9816 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java
@@ -236,7 +236,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
this.clockService = config.getClockService();
DocStoreIntegration docStoreComponents = config.createDocStoreIntegration(this);
- this.transactionManager = config.createTransactionManager(docStoreComponents.updateProcessor());
+ this.transactionManager = config.createTransactionManager(this, docStoreComponents.updateProcessor());
this.documentStore = docStoreComponents.documentStore();
this.queryPlanManager = config.initQueryPlanManager(transactionManager);
this.metaInfoManager = new DefaultMetaInfoManager(this);
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
index 8cbbbfa4b..dca8e3059 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
@@ -423,13 +423,13 @@ public class InternalConfiguration {
/**
* Create the TransactionManager taking into account autoCommit mode.
*/
- TransactionManager createTransactionManager(DocStoreUpdateProcessor indexUpdateProcessor) {
+ TransactionManager createTransactionManager(SpiServer server, DocStoreUpdateProcessor indexUpdateProcessor) {
TransactionScopeManager scopeManager = createTransactionScopeManager();
boolean notifyL2CacheInForeground = cacheManager.isLocalL2Caching() || config.isNotifyL2CacheInForeground();
TransactionManagerOptions options =
- new TransactionManagerOptions(notifyL2CacheInForeground, config, scopeManager, clusterManager, backgroundExecutor,
+ new TransactionManagerOptions(server, notifyL2CacheInForeground, config, scopeManager, clusterManager, backgroundExecutor,
indexUpdateProcessor, beanDescriptorManager, dataSource(), profileHandler(), logManager,
tableModState, cacheNotify, clockService);
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java
index df92246fa..27eea2e4c 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DefaultPersistenceContext.java
@@ -1,11 +1,9 @@
package io.ebeaninternal.server.transaction;
+import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
/**
@@ -200,6 +198,20 @@ public final class DefaultPersistenceContext implements PersistenceContext {
}
}
+ @Override
+ public List dirtyBeans() {
+ lock.lock();
+ try {
+ List list = new ArrayList<>();
+ for (ClassContext classContext : typeCache.values()) {
+ classContext.dirtyBeans(list);
+ }
+ return list;
+ } finally {
+ lock.unlock();
+ }
+ }
+
@Override
public String toString() {
lock.lock();
@@ -318,6 +330,17 @@ public final class DefaultPersistenceContext implements PersistenceContext {
deleteSet.add(id);
map.remove(id);
}
+
+ /**
+ * Add the dirty beans to the list.
+ */
+ void dirtyBeans(List list) {
+ for (Object value : map.values()) {
+ if (((EntityBean) value)._ebean_getIntercept().isDirty()) {
+ list.add(value);
+ }
+ }
+ }
}
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
index 79bf85335..04d7a83a5 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
@@ -95,6 +95,11 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
return startNanos;
}
+ @Override
+ public void setTransparentPersistence(boolean transparentPersistence) {
+ // do nothing
+ }
+
@Override
public void setLabel(String label) {
// do nothing
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
index 8c1897ff5..2585c2b23 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
@@ -187,6 +187,8 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
private final long startNanos;
+ private boolean transparentPersistence;
+
/**
* Create a new JdbcTransaction.
*/
@@ -294,6 +296,12 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
return sb.toString();
}
+ @Override
+ public void setTransparentPersistence(boolean transparentPersistence) {
+ this.transparentPersistence = transparentPersistence;
+ this.batchMode = true;
+ }
+
@Override
public boolean isSkipCacheExplicit() {
return (skipCache != null && !skipCache);
@@ -774,6 +782,10 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
* Flush the JDBC batch and execute derived relationship statements if necessary.
*/
private void internalBatchFlush() {
+ if (transparentPersistence) {
+ // Experimental - flush dirty beans held by the persistence context
+ manager.flushTransparent(persistenceContext, this);
+ }
batchFlush();
if (deferredList != null) {
for (PersistDeferredRelationship deferred : deferredList) {
@@ -1042,7 +1054,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
throw new IllegalStateException(illegalStateMessage);
}
try {
- if (queryOnly) {
+ if (queryOnly && !transparentPersistence) {
connectionEndForQueryOnly();
} else {
flushCommitAndNotify();
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java
index 2ef3f2058..5d593302f 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java
@@ -25,6 +25,11 @@ class NoTransaction implements SpiTransaction {
static final NoTransaction INSTANCE = new NoTransaction();
+ @Override
+ public void setTransparentPersistence(boolean transparentPersistence) {
+ // do nothing
+ }
+
@Override
public void setLabel(String label) {
// do nothing
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java
index b50122f5c..7b596afec 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/SavepointTransaction.java
@@ -60,6 +60,11 @@ class SavepointTransaction extends SpiTransactionProxy {
this.rollbackOnly = true;
}
+ @Override
+ public void setTransparentPersistence(boolean transparentPersistence) {
+ throw new IllegalStateException("This is not handled yet. Need to review this case.");
+ }
+
@Override
public void commit() {
if (rollbackOnly) {
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
index e42ce2fe3..45885ad2a 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
@@ -5,6 +5,7 @@ import io.ebean.ProfileLocation;
import io.ebean.TxScope;
import io.ebean.annotation.PersistBatch;
import io.ebean.annotation.TxType;
+import io.ebean.bean.PersistenceContext;
import io.ebean.cache.ServerCacheNotification;
import io.ebean.cache.ServerCacheNotify;
import io.ebean.config.CurrentTenantProvider;
@@ -17,6 +18,7 @@ import io.ebean.meta.MetricVisitor;
import io.ebean.metric.MetricFactory;
import io.ebean.metric.TimedMetric;
import io.ebean.metric.TimedMetricMap;
+import io.ebean.plugin.SpiServer;
import io.ebeaninternal.api.ScopeTrans;
import io.ebeaninternal.api.ScopedTransaction;
import io.ebeaninternal.api.SpiLogManager;
@@ -59,6 +61,8 @@ public class TransactionManager implements SpiTransactionManager {
private static final Logger clusterLogger = LoggerFactory.getLogger("io.ebean.Cluster");
+ private final SpiServer server;
+
private final BeanDescriptorManager beanDescriptorManager;
/**
@@ -153,7 +157,7 @@ public class TransactionManager implements SpiTransactionManager {
* Create the TransactionManager
*/
public TransactionManager(TransactionManagerOptions options) {
-
+ this.server = options.server;
this.logManager = options.logManager;
this.txnLogger = logManager.txn();
this.txnDebug = txnLogger.isDebug();
@@ -786,4 +790,14 @@ public class TransactionManager implements SpiTransactionManager {
public boolean isLogSummary() {
return logManager.sum().isDebug();
}
+
+ /**
+ * Experimental - find dirty beans in the persistence context and persist them.
+ */
+ public void flushTransparent(PersistenceContext persistenceContext, SpiTransaction transaction) {
+ List dirtyBeans = persistenceContext.dirtyBeans();
+ if (!dirtyBeans.isEmpty()) {
+ server.updateAll(dirtyBeans, transaction);
+ }
+ }
}
diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
index 9a1b0139b..5d0c87794 100644
--- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
+++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
@@ -3,6 +3,7 @@ package io.ebeaninternal.server.transaction;
import io.ebean.BackgroundExecutor;
import io.ebean.cache.ServerCacheNotify;
import io.ebean.config.DatabaseConfig;
+import io.ebean.plugin.SpiServer;
import io.ebeaninternal.api.SpiLogManager;
import io.ebeaninternal.api.SpiProfileHandler;
import io.ebeaninternal.server.cluster.ClusterManager;
@@ -15,6 +16,7 @@ import io.ebeanservice.docstore.api.DocStoreUpdateProcessor;
*/
public class TransactionManagerOptions {
+ final SpiServer server;
final boolean notifyL2CacheInForeground;
final DatabaseConfig config;
final ClusterManager clusterManager;
@@ -31,11 +33,11 @@ public class TransactionManagerOptions {
final ClockService clockService;
- public TransactionManagerOptions(boolean notifyL2CacheInForeground, DatabaseConfig config, TransactionScopeManager scopeManager, ClusterManager clusterManager,
+ public TransactionManagerOptions(SpiServer server, boolean notifyL2CacheInForeground, DatabaseConfig config, TransactionScopeManager scopeManager, ClusterManager clusterManager,
BackgroundExecutor backgroundExecutor, DocStoreUpdateProcessor docStoreUpdateProcessor,
BeanDescriptorManager descMgr, DataSourceSupplier dataSourceSupplier, SpiProfileHandler profileHandler,
SpiLogManager logManager, TableModState tableModState, ServerCacheNotify cacheNotify, ClockService clockService) {
-
+ this.server = server;
this.notifyL2CacheInForeground = notifyL2CacheInForeground;
this.config = config;
this.scopeManager = scopeManager;
diff --git a/ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java b/ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java
new file mode 100644
index 000000000..2922d20b2
--- /dev/null
+++ b/ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java
@@ -0,0 +1,204 @@
+package org.tests.transparentpersist;
+
+import io.ebean.BaseTestCase;
+import io.ebean.DB;
+import io.ebean.Database;
+import io.ebean.Transaction;
+import io.ebeaninternal.api.SpiTransaction;
+import io.ebeantest.LoggedSql;
+import org.junit.Test;
+import org.tests.model.basic.Customer;
+import org.tests.model.basic.EBasicVer;
+import org.tests.model.basic.Order;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestTransparentPersist extends BaseTestCase {
+
+ @Test
+ public void simpleInsertUpdateDelete_experimental() {
+
+ EBasicVer b0 = new EBasicVer("simpleIUD_0");
+ b0.save();
+ EBasicVer b1 = new EBasicVer("simpleIUD_1");
+ b1.save();
+ EBasicVer b2 = new EBasicVer("simpleIUD_2");
+ b2.save();
+
+ EBasicVer newBean;
+ try (Transaction transaction = DB.beginTransaction()) {
+ transaction.setTransparentPersistence(true); // EXPERIMENTAL feature
+
+ EBasicVer found = DB.find(EBasicVer.class, b0.getId());
+ found.setName("auto dirty");
+
+ // delete by id
+ DB.delete(EBasicVer.class, b1.getId());
+ // find and delete, note the delete is batched up to execute later
+ DB.delete(DB.find(EBasicVer.class, b2.getId()));
+
+ // insert is batched up to execute later
+ newBean = new EBasicVer("simpleIUD_New1");
+ DB.save(newBean);
+ // can still mutate newBean before flush (but not after flush yet as new bean isn't put into Persistence context)
+ newBean.setName("simpleIUD_New2");
+
+ transaction.commit();
+ }
+
+ EBasicVer after = DB.find(EBasicVer.class, b0.getId());
+ assertThat(after.getName()).isEqualTo("auto dirty");
+
+ EBasicVer wasInserted = DB.find(EBasicVer.class, newBean.getId());
+ assertThat(wasInserted.getName()).isEqualTo("simpleIUD_New2");
+
+ assertThat(DB.find(EBasicVer.class, b1.getId())).isNull();
+ assertThat(DB.find(EBasicVer.class, b2.getId())).isNull();
+
+ DB.delete(after);
+ DB.delete(wasInserted);
+ }
+
+ @Test
+ public void simpleUpdate_experimental() {
+
+ EBasicVer transPersist = new EBasicVer("simulate_simpleUpdate");
+ transPersist.save();
+
+ try (Transaction transaction = DB.beginTransaction()) {
+ transaction.setTransparentPersistence(true); // EXPERIMENTAL feature
+
+ EBasicVer found = DB.find(EBasicVer.class, transPersist.getId());
+ found.setName("Persisted automatically as dirty");
+
+ transaction.commit();
+ }
+
+ EBasicVer after = DB.find(EBasicVer.class,transPersist.getId());
+ assertThat(after.getName()).isEqualTo("Persisted automatically as dirty");
+ DB.delete(after);
+ }
+
+ @Test
+ public void updateWithPersistCascadeInsert() {
+
+ // setup data
+ Customer c0 = new Customer();
+ c0.setName("firstCust");
+ Order order = new Order();
+ order.setStatus(Order.Status.NEW);
+ order.setCustomer(c0);
+ DB.save(order);
+
+ try (Transaction transaction = DB.beginTransaction()) {
+ transaction.setTransparentPersistence(true); // EXPERIMENTAL feature
+
+ Order foundOrder = DB.find(Order.class, order.getId());
+ foundOrder.setStatus(Order.Status.APPROVED);
+ // cascade persist will insert this customer (even though it isn't in the persistence context)
+ Customer c1 = new Customer();
+ c1.setName("newCust CascadePersist");
+ foundOrder.setCustomer(c1);
+
+ transaction.commit();
+ }
+
+ Order checkOrder = DB.find(Order.class, order.getId());
+
+ assertThat(checkOrder.getStatus()).isEqualTo(Order.Status.APPROVED);
+ assertThat(checkOrder.getCustomer().getName()).isEqualTo("newCust CascadePersist");
+
+ DB.delete(checkOrder);
+ DB.delete(Customer.class, checkOrder.getCustomer().getId());
+ DB.delete(Customer.class, c0.getId());
+ }
+
+ @Test
+ public void updateReferenceOnlyWithPersistCascade_Insert_andUpdateForeignKey() {
+
+ // setup data
+ Customer c0 = new Customer();
+ c0.setName("firstCust");
+ Order order = new Order();
+ order.setStatus(Order.Status.NEW);
+ order.setCustomer(c0);
+ DB.save(order);
+
+ LoggedSql.start();
+
+ try (Transaction transaction = DB.beginTransaction()) {
+ transaction.setTransparentPersistence(true); // EXPERIMENTAL feature
+
+ Order foundOrder = DB.find(Order.class, order.getId());
+ // we ONLY mutate the foreign key
+ // cascade persist will insert this customer (even though it isn't in the persistence context)
+ Customer c1 = new Customer();
+ c1.setName("newCust CascadePersist");
+ foundOrder.setCustomer(c1);
+
+ transaction.commit();
+ }
+
+ List sql = LoggedSql.stop();
+
+ Order checkOrder = DB.find(Order.class, order.getId());
+
+ assertThat(checkOrder.getStatus()).isEqualTo(Order.Status.NEW);
+ assertThat(checkOrder.getCustomer().getName()).isEqualTo("newCust CascadePersist");
+
+ assertThat(sql).hasSize(5);
+ assertThat(sql.get(0)).contains("select t0.id, t0.status, t0.order_date");
+ assertThat(sql.get(1)).contains("insert into o_customer");
+ assertThat(sql.get(2)).contains(" -- bind(");
+ assertThat(sql.get(3)).contains("update o_order set updtime=?, kcustomer_id=? where id=? and updtime=?");
+ assertThat(sql.get(4)).contains(" -- bind(");
+
+ DB.delete(checkOrder);
+ DB.delete(Customer.class, checkOrder.getCustomer().getId());
+ DB.delete(Customer.class, c0.getId());
+ }
+
+ @Test
+ public void simulate_transparentPersistence_forSimpleUpdate() {
+
+ EBasicVer transPersist = new EBasicVer("simulate_simpleUpdate");
+ transPersist.save();
+
+ try (Transaction transaction = DB.beginTransaction()) {
+ transaction.setBatchMode(true);
+ transaction.setBatchSize(10);
+
+ EBasicVer found = DB.find(EBasicVer.class, transPersist.getId());
+ found.setName("Changed");
+
+ // simulate transparent persistence
+ List dirtyBeans = simulateTransparentPersist(transaction);
+ assertThat(dirtyBeans).hasSize(1);
+ assertThat(dirtyBeans).contains(found);
+
+ // would occur as first part of flush
+ transaction.flush();
+ transaction.commit();
+ }
+
+ EBasicVer after = DB.find(EBasicVer.class,transPersist.getId());
+ assertThat(after.getName()).isEqualTo("Changed");
+ DB.delete(after);
+ }
+
+ private List simulateTransparentPersist(Transaction transaction) {
+ Database db = DB.getDefault();
+ List dirtyBeans = getDirtyBeansFromPersistenceContext(transaction);
+ for (Object dirtyBean : dirtyBeans) {
+ db.update(dirtyBean, transaction);
+ }
+ return dirtyBeans;
+ }
+
+ private List getDirtyBeansFromPersistenceContext(Transaction transaction) {
+ return ((SpiTransaction)transaction).getPersistenceContext().dirtyBeans();
+ }
+
+}
diff --git a/ebean-core/src/test/resources/logback-test.xml b/ebean-core/src/test/resources/logback-test.xml
index 1b8a60a9b..164ff73d3 100644
--- a/ebean-core/src/test/resources/logback-test.xml
+++ b/ebean-core/src/test/resources/logback-test.xml
@@ -79,8 +79,8 @@
-
-
+
+