#2251 - Transparent persistence of to-many associations (with orphanRemoval)

This commit is contained in:
rbygrave
2021-06-18 16:05:25 +12:00
parent 4aa4137abe
commit 8f432aa0a9
17 changed files with 122 additions and 100 deletions
@@ -0,0 +1,17 @@
package io.ebeaninternal.api;
import io.ebean.bean.EntityBean;
/**
* SPI interface for underlying BeanDescriptor.
*/
public interface SpiBeanType {
/**
* Return true if the bean contains a many property that has modifications.
* <p>
* That is a ManyToMany or a OneToMany with orphan removal with additions
* or removals from the collection.
*/
boolean isToManyDirty(EntityBean bean);
}
@@ -0,0 +1,13 @@
package io.ebeaninternal.api;
/**
* Manager of SpiBeanTypes.
*/
public interface SpiBeanTypeManager {
/**
* Return the bean type for the given entity class.
*/
SpiBeanType getBeanType(Class<?> entityType);
}
@@ -0,0 +1,17 @@
package io.ebeaninternal.api;
import io.ebean.bean.PersistenceContext;
import java.util.List;
/**
* SPI extension to PersistenceContext.
*/
public interface SpiPersistenceContext extends PersistenceContext {
/**
* Return the list of dirty beans held by this persistence context.
*/
List<Object> dirtyBeans(SpiBeanTypeManager manager);
}
@@ -3,7 +3,6 @@ package io.ebeaninternal.api;
import io.ebean.ProfileLocation;
import io.ebean.Transaction;
import io.ebean.annotation.DocStoreMode;
import io.ebean.bean.PersistenceContext;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.server.core.PersistDeferredRelationship;
@@ -196,7 +195,7 @@ public interface SpiTransaction extends Transaction {
* later. This is along the lines of 'extended persistence context'
* behaviour.
*/
PersistenceContext getPersistenceContext();
SpiPersistenceContext getPersistenceContext();
/**
* Set the persistence context to this transaction.
@@ -208,7 +207,7 @@ public interface SpiTransaction extends Transaction {
* and setPersistenceContext() enable a developer to reuse a single
* PersistenceContext with multiple transactions.
*/
void setPersistenceContext(PersistenceContext context);
void setPersistenceContext(SpiPersistenceContext context);
/**
* Return the underlying Connection for internal use.
@@ -3,7 +3,6 @@ package io.ebeaninternal.api;
import io.ebean.ProfileLocation;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.bean.PersistenceContext;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.server.core.PersistDeferredRelationship;
@@ -379,12 +378,12 @@ public abstract class SpiTransactionProxy implements SpiTransaction {
}
@Override
public PersistenceContext getPersistenceContext() {
public SpiPersistenceContext getPersistenceContext() {
return transaction.getPersistenceContext();
}
@Override
public void setPersistenceContext(PersistenceContext context) {
public void setPersistenceContext(SpiPersistenceContext context) {
transaction.setPersistenceContext(context);
}
@@ -38,15 +38,7 @@ import io.ebean.plugin.BeanType;
import io.ebean.plugin.ExpressionPath;
import io.ebean.plugin.Property;
import io.ebean.util.SplitName;
import io.ebeaninternal.api.BeanCacheResult;
import io.ebeaninternal.api.CQueryPlanKey;
import io.ebeaninternal.api.ConcurrencyMode;
import io.ebeaninternal.api.LoadBeanContext;
import io.ebeaninternal.api.LoadContext;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.SpiUpdatePlan;
import io.ebeaninternal.api.*;
import io.ebeaninternal.api.TransactionEventTable.TableIUD;
import io.ebeaninternal.api.json.SpiJsonReader;
import io.ebeaninternal.api.json.SpiJsonWriter;
@@ -119,7 +111,7 @@ import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero;
/**
* Describes Beans including their deployment information.
*/
public class BeanDescriptor<T> implements BeanType<T>, STreeType {
public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
private static final Logger logger = LoggerFactory.getLogger(BeanDescriptor.class);
@@ -1880,7 +1872,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
if (refBean == null) {
refBean = createReference(readOnly, false, id, pc);
}
return (EntityBean)refBean;
return (EntityBean) refBean;
}
/**
@@ -2937,6 +2929,20 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType {
return false;
}
@Override
public boolean isToManyDirty(EntityBean bean) {
final EntityBeanIntercept ebi = bean._ebean_getIntercept();
for (BeanPropertyAssocMany<?> many : propertiesManySave) {
if (ebi.isLoadedProperty(many.getPropertyIndex())) {
final BeanCollection<?> value = (BeanCollection<?>) many.getValue(bean);
if (value != null && value.hasModifications()) {
return true;
}
}
}
return false;
}
/**
* Return true if the bean is draftable and considered a 'live' instance.
*/
@@ -26,9 +26,7 @@ import io.ebean.meta.MetricVisitor;
import io.ebean.meta.QueryPlanInit;
import io.ebean.plugin.BeanType;
import io.ebean.util.AnnotationUtil;
import io.ebeaninternal.api.ConcurrencyMode;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.TransactionEventTable;
import io.ebeaninternal.api.*;
import io.ebeaninternal.server.cache.CacheChangeSet;
import io.ebeaninternal.server.cache.SpiCacheManager;
import io.ebeaninternal.server.core.InternString;
@@ -88,7 +86,7 @@ import java.util.concurrent.TimeUnit;
/**
* Creates BeanDescriptors.
*/
public class BeanDescriptorManager implements BeanDescriptorMap {
public class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTypeManager {
private static final Logger logger = LoggerFactory.getLogger(BeanDescriptorManager.class);
@@ -263,6 +261,11 @@ public class BeanDescriptorManager implements BeanDescriptorMap {
return descQueueMap.get(queueId);
}
@Override
public SpiBeanType getBeanType(Class<?> entityType) {
return getBeanDescriptor(entityType);
}
@Override
@SuppressWarnings("unchecked")
public <T> BeanDescriptor<T> getBeanDescriptor(Class<T> entityType) {
@@ -2,6 +2,9 @@ package io.ebeaninternal.server.transaction;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
import io.ebeaninternal.api.SpiBeanType;
import io.ebeaninternal.api.SpiBeanTypeManager;
import io.ebeaninternal.api.SpiPersistenceContext;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
@@ -11,19 +14,16 @@ import java.util.concurrent.locks.ReentrantLock;
* <p>
* Ensures only one instance of a bean is used according to its type and unique
* id.
* </p>
* <p>
* PersistenceContext lives on a Transaction and as such is expected to only
* have a single thread accessing it at a time. This is not expected to be used
* concurrently.
* </p>
* <p>
* Duplicate beans are ones having the same type and unique id value. These are
* considered duplicates and replaced by the bean instance that was already
* loaded into the PersistenceContext.
* </p>
*/
public final class DefaultPersistenceContext implements PersistenceContext {
public final class DefaultPersistenceContext implements SpiPersistenceContext {
/**
* Map used hold caches. One cache per bean type.
@@ -199,12 +199,12 @@ public final class DefaultPersistenceContext implements PersistenceContext {
}
@Override
public List<Object> dirtyBeans() {
public List<Object> dirtyBeans(SpiBeanTypeManager manager) {
lock.lock();
try {
List<Object> list = new ArrayList<>();
for (ClassContext classContext : typeCache.values()) {
classContext.dirtyBeans(list);
classContext.dirtyBeans(manager, list);
}
return list;
} finally {
@@ -223,26 +223,26 @@ public final class DefaultPersistenceContext implements PersistenceContext {
}
private ClassContext getClassContext(Class<?> rootType) {
return typeCache.computeIfAbsent(rootType, k -> new ClassContext());
return typeCache.computeIfAbsent(rootType, k -> new ClassContext(rootType));
}
private static class ClassContext {
private final Map<Object, Object> map = new HashMap<>();
private final Class<?> rootType;
private Set<Object> deleteSet;
private int initialSize;
private ClassContext parent;
private ClassContext() {
private ClassContext(Class<?> rootType) {
this.rootType = rootType;
}
/**
* Create as a shallow copy.
*/
private ClassContext(ClassContext source, boolean initial) {
this.rootType = source.rootType;
if (initial || source.isTransfer()) {
parent = source.transferParent();
initialSize = parent.size();
@@ -334,9 +334,11 @@ public final class DefaultPersistenceContext implements PersistenceContext {
/**
* Add the dirty beans to the list.
*/
void dirtyBeans(List<Object> list) {
void dirtyBeans(SpiBeanTypeManager manager, List<Object> list) {
final SpiBeanType beanType = manager.getBeanType(rootType);
for (Object value : map.values()) {
if (((EntityBean) value)._ebean_getIntercept().isDirty()) {
EntityBean bean = (EntityBean) value;
if (bean._ebean_getIntercept().isDirty() || beanType.isToManyDirty(bean)) {
list.add(value);
}
}
@@ -3,13 +3,9 @@ package io.ebeaninternal.server.transaction;
import io.ebean.ProfileLocation;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.bean.PersistenceContext;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.api.SpiProfileTransactionEvent;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.TransactionEvent;
import io.ebeaninternal.api.TxnProfileEventCodes;
import io.ebeaninternal.api.*;
import io.ebeaninternal.server.core.PersistDeferredRelationship;
import io.ebeaninternal.server.core.PersistRequestBean;
import io.ebeaninternal.server.persist.BatchControl;
@@ -58,7 +54,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
/**
* Holder of the objects fetched to ensure unique objects are used.
*/
private PersistenceContext persistenceContext;
private SpiPersistenceContext persistenceContext;
private Object tenantId;
@@ -421,7 +417,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
* Return the persistence context associated with this transaction.
*/
@Override
public PersistenceContext getPersistenceContext() {
public SpiPersistenceContext getPersistenceContext() {
return persistenceContext;
}
@@ -434,7 +430,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
* </p>
*/
@Override
public void setPersistenceContext(PersistenceContext context) {
public void setPersistenceContext(SpiPersistenceContext context) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
@@ -3,20 +3,16 @@ package io.ebeaninternal.server.transaction;
import io.ebean.ProfileLocation;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.bean.PersistenceContext;
import io.ebean.config.DatabaseConfig;
import io.ebean.config.dbplatform.DatabasePlatform.OnQueryOnly;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.api.SpiProfileTransactionEvent;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.TransactionEvent;
import io.ebeaninternal.api.TxnProfileEventCodes;
import io.ebeaninternal.api.*;
import io.ebeaninternal.server.core.PersistDeferredRelationship;
import io.ebeaninternal.server.core.PersistRequestBean;
import io.ebeaninternal.server.util.Str;
import io.ebeaninternal.server.persist.BatchControl;
import io.ebeaninternal.server.persist.BatchedSqlException;
import io.ebeaninternal.server.util.Str;
import io.ebeanservice.docstore.api.DocStoreTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,12 +21,7 @@ import javax.persistence.PersistenceException;
import javax.persistence.RollbackException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Consumer;
/**
@@ -99,7 +90,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
/**
* Holder of the objects fetched to ensure unique objects are used.
*/
private PersistenceContext persistenceContext;
private SpiPersistenceContext persistenceContext;
/**
* Used to give developers more control over the insert update and delete
@@ -806,7 +797,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
* Return the persistence context associated with this transaction.
*/
@Override
public PersistenceContext getPersistenceContext() {
public SpiPersistenceContext getPersistenceContext() {
return persistenceContext;
}
@@ -816,10 +807,9 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
* This could be considered similar to EJB3 Extended PersistanceContext. In
* that you get the PersistanceContext from a transaction, hold onto it, and
* then set it back later to a second transaction.
* </p>
*/
@Override
public void setPersistenceContext(PersistenceContext context) {
public void setPersistenceContext(SpiPersistenceContext context) {
if (!isActive()) {
throw new IllegalStateException(illegalStateMessage);
}
@@ -3,9 +3,9 @@ package io.ebeaninternal.server.transaction;
import io.ebean.ProfileLocation;
import io.ebean.TransactionCallback;
import io.ebean.annotation.DocStoreMode;
import io.ebean.bean.PersistenceContext;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebeaninternal.api.SpiPersistenceContext;
import io.ebeaninternal.api.SpiProfileTransactionEvent;
import io.ebeaninternal.api.SpiTransaction;
import io.ebeaninternal.api.TransactionEvent;
@@ -349,12 +349,12 @@ class NoTransaction implements SpiTransaction {
}
@Override
public PersistenceContext getPersistenceContext() {
public SpiPersistenceContext getPersistenceContext() {
return null;
}
@Override
public void setPersistenceContext(PersistenceContext context) {
public void setPersistenceContext(SpiPersistenceContext context) {
}
@Override
@@ -5,7 +5,6 @@ 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;
@@ -19,15 +18,7 @@ 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;
import io.ebeaninternal.api.SpiLogger;
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.*;
import io.ebeaninternal.api.TransactionEventTable.TableIUD;
import io.ebeaninternal.server.cache.CacheChangeSet;
import io.ebeaninternal.server.cluster.ClusterManager;
@@ -770,8 +761,8 @@ public class TransactionManager implements SpiTransactionManager {
/**
* Experimental - find dirty beans in the persistence context and persist them.
*/
public void flushTransparent(PersistenceContext persistenceContext, SpiTransaction transaction) {
List<Object> dirtyBeans = persistenceContext.dirtyBeans();
public void flushTransparent(SpiPersistenceContext persistenceContext, SpiTransaction transaction) {
List<Object> dirtyBeans = persistenceContext.dirtyBeans(beanDescriptorManager);
if (!dirtyBeans.isEmpty()) {
server.updateAll(dirtyBeans, transaction);
}