From a617d5890203d445cbd33badcec33ddd3d9d5c0a Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Thu, 1 Apr 2021 14:47:54 +1300 Subject: [PATCH] Initial experimental support for "transparent persistence" In short, when this is turned on at flush() get any dirty beans held by the PersistenceContext and persist them (always update for these beans) --- .../src/main/java/io/ebean/Transaction.java | 8 + .../io/ebean/bean/PersistenceContext.java | 7 + .../ebeaninternal/api/ScopedTransaction.java | 5 + .../server/core/DefaultServer.java | 2 +- .../server/core/InternalConfiguration.java | 4 +- .../DefaultPersistenceContext.java | 31 ++- .../ImplicitReadOnlyTransaction.java | 5 + .../server/transaction/JdbcTransaction.java | 14 +- .../server/transaction/NoTransaction.java | 5 + .../transaction/SavepointTransaction.java | 5 + .../transaction/TransactionManager.java | 16 +- .../TransactionManagerOptions.java | 6 +- .../TestTransparentPersist.java | 204 ++++++++++++++++++ .../src/test/resources/logback-test.xml | 4 +- 14 files changed, 303 insertions(+), 13 deletions(-) create mode 100644 ebean-core/src/test/java/org/tests/transparentpersist/TestTransparentPersist.java 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 @@ - - + +