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 @@ - - + +