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)
This commit is contained in:
Robin Bygrave
2021-04-01 14:47:54 +13:00
parent 7fc4a229ae
commit a617d58902
14 changed files with 303 additions and 13 deletions
@@ -59,6 +59,14 @@ public interface Transaction extends AutoCloseable {
*/
void register(TransactionCallback callback);
/**
* EXPERIMENTAL - turn on transparent persistence and batchMode true.
* <p>
* 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.
* <p>
@@ -1,5 +1,7 @@
package io.ebean.bean;
import java.util.List;
/**
* Holds entity beans by there type and id.
* <p>
@@ -77,6 +79,11 @@ public interface PersistenceContext {
*/
boolean resetLimit();
/**
* Return the list of dirty beans held by this persistence context.
*/
List<Object> dirtyBeans();
/**
* Wrapper on a bean to also indicate if a bean has been deleted.
* <p>
@@ -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();
@@ -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);
@@ -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);
@@ -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<Object> dirtyBeans() {
lock.lock();
try {
List<Object> 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<Object> list) {
for (Object value : map.values()) {
if (((EntityBean) value)._ebean_getIntercept().isDirty()) {
list.add(value);
}
}
}
}
}
@@ -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
@@ -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();
@@ -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
@@ -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) {
@@ -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<Object> dirtyBeans = persistenceContext.dirtyBeans();
if (!dirtyBeans.isEmpty()) {
server.updateAll(dirtyBeans, transaction);
}
}
}
@@ -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;
@@ -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<String> 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<Object> 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<Object> simulateTransparentPersist(Transaction transaction) {
Database db = DB.getDefault();
List<Object> dirtyBeans = getDirtyBeansFromPersistenceContext(transaction);
for (Object dirtyBean : dirtyBeans) {
db.update(dirtyBean, transaction);
}
return dirtyBeans;
}
private List<Object> getDirtyBeansFromPersistenceContext(Transaction transaction) {
return ((SpiTransaction)transaction).getPersistenceContext().dirtyBeans();
}
}
@@ -79,8 +79,8 @@
<logger name="io.ebean.docker" level="TRACE"/>
<!-- <logger name="io.ebean.DDL" level="DEBUG"/>-->
<!-- <logger name="io.ebean.SQL" level="TRACE"/>-->
<!-- <logger name="io.ebean.TXN" level="TRACE"/>-->
<logger name="io.ebean.SQL" level="TRACE"/>
<logger name="io.ebean.TXN" level="TRACE"/>
<!-- <logger name="io.ebean.SUM" level="TRACE"/>-->
<!-- <logger name="io.ebean.cache" level="TRACE"/>-->