diff --git a/ebean-api/src/main/java/io/ebean/ModifyAwareType.java b/ebean-api/src/main/java/io/ebean/ModifyAwareType.java index f846eecf45..cad39026f0 100644 --- a/ebean-api/src/main/java/io/ebean/ModifyAwareType.java +++ b/ebean-api/src/main/java/io/ebean/ModifyAwareType.java @@ -16,4 +16,9 @@ public interface ModifyAwareType { */ void setMarkedDirty(boolean markedDirty); + /** + * Return a unmodifiable version of this type. + */ + Object freeze(); + } diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java index 4f7c148189..6a3afdc851 100644 --- a/ebean-api/src/main/java/io/ebean/Transaction.java +++ b/ebean-api/src/main/java/io/ebean/Transaction.java @@ -2,6 +2,7 @@ import io.ebean.annotation.DocStoreMode; import io.ebean.annotation.PersistBatch; +import io.ebean.bean.FrozenBeans; import io.ebean.config.DatabaseConfig; import io.ebean.config.DocStoreConfig; @@ -54,6 +55,24 @@ static Transaction current() { */ int SERIALIZABLE = java.sql.Connection.TRANSACTION_SERIALIZABLE; + + /** + * Experimental Feature - Freeze the beans in the persistence context and detach them. + */ + @Deprecated(since = "Experimental Feature") + default FrozenBeans freezeAndDetach() { + throw new UnsupportedOperationException(); + } + + /** + * Experimental Feature - Attach frozen beans to the persistence context so that they can be + * used with ORM queries (with the persistence context acting like a cache). + */ + @Deprecated(since = "Experimental Feature") + default void attach(FrozenBeans frozenBeans) { + throw new UnsupportedOperationException(); + } + /** * Register a TransactionCallback with this transaction. */ diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java index 1161476ddd..3046a2b8c7 100644 --- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java @@ -35,6 +35,11 @@ enum ModifyListenMode { ALL } + /** + * Return a unmodifiable collection of the underlying entities. + */ + Object freeze(); + /** * Set the disableLazyLoad state. */ diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 1127c02b25..5867f63021 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -552,4 +552,16 @@ public interface EntityBeanIntercept extends Serializable { * Update the 'next' mutable info returning the content that was obtained via dirty detection. */ String mutableNext(int propertyIndex); + + /** + * Set that lazy loading if invoked throws a PersistenceException. + */ + void errorOnLazyLoad(boolean lazyLoadAsError); + + /** + * Freeze the intercept so that it is readOnly. + * Lazy loading and mutation of the bean is not allowed. + */ + void freeze(); + } diff --git a/ebean-api/src/main/java/io/ebean/bean/FrozenBeans.java b/ebean-api/src/main/java/io/ebean/bean/FrozenBeans.java new file mode 100644 index 0000000000..fa485c51cf --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/FrozenBeans.java @@ -0,0 +1,9 @@ +package io.ebean.bean; + +import java.io.Serializable; + +/** + * Holds entity beans that are frozen. + */ +public interface FrozenBeans extends Serializable { +} diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java new file mode 100644 index 0000000000..ea3faee4a3 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java @@ -0,0 +1,343 @@ +package io.ebean.bean; + +import io.ebean.DB; +import io.ebean.Database; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceException; + +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Base EntityBeanIntercept that supports partial loaded state and lazy loading. + */ +abstract class InterceptBase implements EntityBeanIntercept { + + /** + * Flag for loaded property. + */ + static final byte FLAG_LOADED_PROP = 1; + + /** + * The actual entity bean that 'owns' this intercept. + */ + protected final EntityBean owner; + protected final byte[] flags; + protected boolean readOnly; + protected boolean disableLazyLoad; + protected boolean errorOnLazyLoad; + + private final ReentrantLock lock = new ReentrantLock(); + private String ebeanServerName; + private transient BeanLoader beanLoader; + private transient PersistenceContext persistenceContext; + + /** + * Flag set when lazy loading failed due to the underlying bean being deleted in the DB. + */ + protected boolean lazyLoadFailure; + protected boolean fullyLoadedBean; + protected int lazyLoadProperty = -1; + protected Object ownerId; + + /** + * Create with a given entity. + */ + InterceptBase(Object ownerBean) { + this.owner = (EntityBean) ownerBean; + this.flags = new byte[owner._ebean_getPropertyNames().length]; + } + + /** + * EXPERIMENTAL - Constructor only for use by serialization frameworks. + */ + InterceptBase() { + this.owner = null; + this.flags = null; + } + + public void freeze() { + this.errorOnLazyLoad = true; + this.readOnly = true; + this.beanLoader = null; + this.persistenceContext = null; + } + + @Override + public final EntityBean owner() { + return owner; + } + + @Override + public final Object ownerId() { + return ownerId; + } + + @Override + public final void setOwnerId(Object ownerId) { + this.ownerId = ownerId; + } + + @Override + public final void setBeanLoader(BeanLoader beanLoader) { + this.beanLoader = beanLoader; + this.ebeanServerName = beanLoader.name(); + } + + @Override + public final void setBeanLoader(BeanLoader beanLoader, PersistenceContext ctx) { + this.beanLoader = beanLoader; + this.persistenceContext = ctx; + this.ebeanServerName = beanLoader.name(); + } + + @Override + public final PersistenceContext persistenceContext() { + return persistenceContext; + } + + @Override + public final void setPersistenceContext(PersistenceContext persistenceContext) { + this.persistenceContext = persistenceContext; + } + + + @Override + public final boolean isFullyLoadedBean() { + return fullyLoadedBean; + } + + @Override + public final void setFullyLoadedBean(boolean fullyLoadedBean) { + this.fullyLoadedBean = fullyLoadedBean; + } + + @Override + public final boolean isPartial() { + for (byte flag : flags) { + if ((flag & FLAG_LOADED_PROP) == 0) { + return true; + } + } + return false; + } + + @Override + public final boolean hasIdOnly(int idIndex) { + for (int i = 0; i < flags.length; i++) { + if (i == idIndex) { + if ((flags[i] & FLAG_LOADED_PROP) == 0) return false; + } else if ((flags[i] & FLAG_LOADED_PROP) != 0) { + return false; + } + } + return true; + } + + @Override + public final boolean isReadOnly() { + return readOnly; + } + + @Override + public final void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + @Override + public final void setLazyLoadFailure(Object ownerId) { + this.lazyLoadFailure = true; + this.ownerId = ownerId; + } + + @Override + public final boolean isLazyLoadFailure() { + return lazyLoadFailure; + } + + @Override + public final boolean isDisableLazyLoad() { + return disableLazyLoad; + } + + @Override + public final void setDisableLazyLoad(boolean disableLazyLoad) { + this.disableLazyLoad = disableLazyLoad; + } + + @Override + public final void errorOnLazyLoad(boolean errorOnLazyLoad) { + this.errorOnLazyLoad = errorOnLazyLoad; + } + + @Override + public final void setEmbeddedLoaded(Object embeddedBean) { + if (embeddedBean instanceof EntityBean) { + ((EntityBean) embeddedBean)._ebean_getIntercept().setLoaded(); + } + } + + @Override + public final int findProperty(String propertyName) { + final String[] names = owner._ebean_getPropertyNames(); + for (int i = 0; i < names.length; i++) { + if (names[i].equals(propertyName)) { + return i; + } + } + return -1; + } + + @Override + public final String property(int propertyIndex) { + if (propertyIndex == -1) { + return null; + } + return owner._ebean_getPropertyName(propertyIndex); + } + + @Override + public final int propertyLength() { + return flags.length; + } + + @Override + public final void setPropertyLoaded(String propertyName, boolean loaded) { + final int position = findProperty(propertyName); + if (position == -1) { + throw new IllegalArgumentException("Property " + propertyName + " not found"); + } + if (loaded) { + flags[position] |= FLAG_LOADED_PROP; + } else { + flags[position] &= ~FLAG_LOADED_PROP; + } + } + + @Override + public final void setPropertyUnloaded(int propertyIndex) { + flags[propertyIndex] &= ~FLAG_LOADED_PROP; + } + + @Override + public final void initialisedMany(int propertyIndex) { + flags[propertyIndex] |= FLAG_LOADED_PROP; + } + + @Override + public final void setLoadedProperty(int propertyIndex) { + flags[propertyIndex] |= FLAG_LOADED_PROP; + } + + @Override + public final void setLoadedPropertyAll() { + for (int i = 0; i < flags.length; i++) { + flags[i] |= FLAG_LOADED_PROP; + } + } + + @Override + public final boolean isLoadedProperty(int propertyIndex) { + return (flags[propertyIndex] & FLAG_LOADED_PROP) != 0; + } + + @Override + public final Set loadedPropertyNames() { + if (fullyLoadedBean) { + return null; + } + final Set props = new LinkedHashSet<>(); + for (int i = 0; i < flags.length; i++) { + if ((flags[i] & FLAG_LOADED_PROP) != 0) { + props.add(property(i)); + } + } + return props; + } + + + @Override + public final StringBuilder loadedPropertyKey() { + final StringBuilder sb = new StringBuilder(); + final int len = propertyLength(); + for (int i = 0; i < len; i++) { + if (isLoadedProperty(i)) { + sb.append(i).append(','); + } + } + return sb; + } + + @Override + public final boolean[] loaded() { + final boolean[] ret = new boolean[flags.length]; + for (int i = 0; i < ret.length; i++) { + ret[i] = (flags[i] & FLAG_LOADED_PROP) != 0; + } + return ret; + } + + @Override + public final int lazyLoadPropertyIndex() { + return lazyLoadProperty; + } + + @Override + public final String lazyLoadProperty() { + return property(lazyLoadProperty); + } + + @Override + public final void loadBean(int loadProperty) { + lock.lock(); + try { + if (errorOnLazyLoad) { + throw new PersistenceException("Lazy loading not allowed on this bean"); + } + if (beanLoader == null) { + final Database database = DB.byName(ebeanServerName); + if (database == null) { + throw new PersistenceException(ebeanServerName == null ? "No registered default server" : "Database [" + ebeanServerName + "] is not registered"); + } + // For stand alone reference bean or after deserialisation lazy load + // using the ebeanServer. Synchronise only on the bean. + loadBeanInternal(loadProperty, database.pluginApi().beanLoader()); + return; + } + } finally { + lock.unlock(); + } + final Lock lock = beanLoader.lock(); + try { + // Lazy loading using LoadBeanContext which supports batch loading + // Synchronise on the beanLoader (a 'node' of the LoadBeanContext 'tree') + loadBeanInternal(loadProperty, beanLoader); + } finally { + lock.unlock(); + } + } + + @Override + public final void loadBeanInternal(int loadProperty, BeanLoader loader) { + if ((flags[loadProperty] & FLAG_LOADED_PROP) != 0) { + // race condition where multiple threads calling preGetter concurrently + return; + } + if (lazyLoadFailure) { + // failed when batch lazy loaded by another bean in the batch + throw new EntityNotFoundException("(Lazy) loading failed on type:" + owner.getClass().getName() + " id:" + ownerId + " - Bean has been deleted. BeanLoader: " + beanLoader); + } + if (lazyLoadProperty == -1) { + lazyLoadProperty = loadProperty; + loader.loadBean(this); + if (lazyLoadFailure) { + // failed when lazy loading this bean + throw new EntityNotFoundException("Lazy loading failed on type:" + owner.getClass().getName() + " id:" + ownerId + " - Bean has been deleted. BeanLoader: " + beanLoader); + } + // bean should be loaded and intercepting now. setLoaded() has + // been called by the lazy loading mechanism + } + } + +} diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java index 26f5faf8d5..d9150c0230 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java @@ -10,103 +10,133 @@ * EntityBeanIntercept optimised for read only use. *

* For the read only use this intercept doesn't need to hold any state that is normally - * required for updates such as per property changed, loaded, dirty state, original values + * required for updates such as per property changed, dirty state, original values * bean state etc. */ -public class InterceptReadOnly implements EntityBeanIntercept { - - private final EntityBean owner; +public final class InterceptReadOnly extends InterceptBase { /** * Create with a given entity. */ public InterceptReadOnly(Object ownerBean) { - this.owner = (EntityBean) ownerBean; + super(ownerBean); } @Override - public String toString() { - return "InterceptReadOnly{" + owner + '}'; + public void preGetId() { + // do nothing } @Override - public EntityBean owner() { - return owner; + public void preGetter(int propertyIndex) { + if (disableLazyLoad) { + return; + } + if (!isLoadedProperty(propertyIndex)) { + loadBean(propertyIndex); + } } - @Override - public PersistenceContext persistenceContext() { - return null; + private void preSetter(int propertyIndex) { + if (readOnly) { + throw new IllegalStateException("This bean is readOnly"); + } + setLoadedProperty(propertyIndex); } @Override - public void setPersistenceContext(PersistenceContext persistenceContext) { - + public void preSetterMany(boolean interceptField, int propertyIndex, Object oldValue, Object newValue) { + preSetter(propertyIndex); } @Override - public void setNodeUsageCollector(NodeUsageCollector usageCollector) { - + public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) { + preSetter(propertyIndex); } @Override - public Object ownerId() { - return null; + public void preSetter(boolean intercept, int propertyIndex, boolean oldValue, boolean newValue) { + preSetter(propertyIndex); } @Override - public void setOwnerId(Object ownerId) { - + public void preSetter(boolean intercept, int propertyIndex, int oldValue, int newValue) { + preSetter(propertyIndex); } @Override - public Object embeddedOwner() { - return null; + public void preSetter(boolean intercept, int propertyIndex, long oldValue, long newValue) { + preSetter(propertyIndex); } @Override - public int embeddedOwnerIndex() { - return 0; + public void preSetter(boolean intercept, int propertyIndex, double oldValue, double newValue) { + preSetter(propertyIndex); } @Override - public void clearGetterCallback() { + public void preSetter(boolean intercept, int propertyIndex, float oldValue, float newValue) { + preSetter(propertyIndex); + } + @Override + public void preSetter(boolean intercept, int propertyIndex, short oldValue, short newValue) { + preSetter(propertyIndex); } @Override - public void registerGetterCallback(PreGetterCallback getterCallback) { + public void preSetter(boolean intercept, int propertyIndex, char oldValue, char newValue) { + preSetter(propertyIndex); + } + @Override + public void preSetter(boolean intercept, int propertyIndex, byte oldValue, byte newValue) { + preSetter(propertyIndex); } @Override - public void setEmbeddedOwner(EntityBean parentBean, int embeddedOwnerIndex) { + public void preSetter(boolean intercept, int propertyIndex, char[] oldValue, char[] newValue) { + preSetter(propertyIndex); + } + @Override + public void preSetter(boolean intercept, int propertyIndex, byte[] oldValue, byte[] newValue) { + preSetter(propertyIndex); } + + // ------------------------------------------------------------------------------------ + // The following method have "Do Nothing" behaviour as not interested in mutation state + // ------------------------------------------------------------------------------------ + @Override - public void setBeanLoader(BeanLoader beanLoader, PersistenceContext ctx) { + public void setNodeUsageCollector(NodeUsageCollector usageCollector) { } @Override - public void setBeanLoader(BeanLoader beanLoader) { + public Object embeddedOwner() { + return null; + } + @Override + public int embeddedOwnerIndex() { + return 0; } @Override - public boolean isFullyLoadedBean() { - return false; + public void clearGetterCallback() { + } @Override - public void setFullyLoadedBean(boolean fullyLoadedBean) { + public void registerGetterCallback(PreGetterCallback getterCallback) { } @Override - public boolean isPartial() { - return false; + public void setEmbeddedOwner(EntityBean parentBean, int embeddedOwnerIndex) { + } @Override @@ -134,11 +164,6 @@ public boolean isNewOrDirty() { return false; } - @Override - public boolean hasIdOnly(int idIndex) { - return false; - } - @Override public boolean isReference() { return false; @@ -159,16 +184,6 @@ public boolean isLoadedFromCache() { return false; } - @Override - public boolean isReadOnly() { - return true; - } - - @Override - public void setReadOnly(boolean readOnly) { - - } - @Override public void setForceUpdate(boolean forceUpdate) { @@ -199,31 +214,6 @@ public void setLoadedLazy() { } - @Override - public void setLazyLoadFailure(Object ownerId) { - - } - - @Override - public boolean isLazyLoadFailure() { - return false; - } - - @Override - public boolean isDisableLazyLoad() { - return false; - } - - @Override - public void setDisableLazyLoad(boolean disableLazyLoad) { - - } - - @Override - public void setEmbeddedLoaded(Object embeddedBean) { - - } - @Override public boolean isEmbeddedNewOrDirty(Object embeddedBean) { return false; @@ -234,45 +224,6 @@ public Object origValue(int propertyIndex) { return null; } - @Override - public int findProperty(String propertyName) { - return 0; - } - - @Override - public String property(int propertyIndex) { - return null; - } - - @Override - public int propertyLength() { - return 0; - } - - @Override - public void setPropertyLoaded(String propertyName, boolean loaded) { - - } - - @Override - public void setPropertyUnloaded(int propertyIndex) { - - } - - @Override - public void setLoadedProperty(int propertyIndex) { - - } - - @Override - public void setLoadedPropertyAll() { - - } - - @Override - public boolean isLoadedProperty(int propertyIndex) { - return false; - } @Override public boolean isChangedProperty(int propertyIndex) { @@ -319,11 +270,6 @@ public void setNewBeanForUpdate() { } - @Override - public Set loadedPropertyNames() { - return Collections.emptySet(); - } - @Override public boolean[] dirtyProperties() { return new boolean[0]; @@ -369,61 +315,11 @@ public void addDirtyPropertyKey(StringBuilder sb) { } - @Override - public StringBuilder loadedPropertyKey() { - return null; - } - - @Override - public boolean[] loaded() { - return new boolean[0]; - } - - @Override - public int lazyLoadPropertyIndex() { - return 0; - } - - @Override - public String lazyLoadProperty() { - return null; - } - - @Override - public void loadBean(int loadProperty) { - - } - - @Override - public void loadBeanInternal(int loadProperty, BeanLoader loader) { - - } - - @Override - public void initialisedMany(int propertyIndex) { - - } - @Override public void preGetterCallback(int propertyIndex) { } - @Override - public void preGetId() { - - } - - @Override - public void preGetter(int propertyIndex) { - - } - - @Override - public void preSetterMany(boolean interceptField, int propertyIndex, Object oldValue, Object newValue) { - - } - @Override public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) { @@ -434,61 +330,6 @@ public void setDirtyStatus() { } - @Override - public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, boolean oldValue, boolean newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, int oldValue, int newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, long oldValue, long newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, double oldValue, double newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, float oldValue, float newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, short oldValue, short newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, char oldValue, char newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, byte oldValue, byte newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, char[] oldValue, char[] newValue) { - - } - - @Override - public void preSetter(boolean intercept, int propertyIndex, byte[] oldValue, byte[] newValue) { - - } - @Override public void setOldValue(int propertyIndex, Object oldValue) { diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java index 3649e70298..e48ff18dca 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java @@ -1,11 +1,7 @@ package io.ebean.bean; -import io.ebean.DB; -import io.ebean.Database; import io.ebean.ValuePair; -import jakarta.persistence.EntityNotFoundException; -import jakarta.persistence.PersistenceException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -13,8 +9,6 @@ import java.math.BigDecimal; import java.net.URL; import java.util.*; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * This is the object added to every entity bean using byte code enhancement. @@ -22,7 +16,7 @@ * This provides the mechanisms to support deferred fetching of reference beans * and oldValues generation for concurrency checking. */ -public final class InterceptReadWrite implements EntityBeanIntercept { +public final class InterceptReadWrite extends InterceptBase { private static final long serialVersionUID = -3664031775464862649L; @@ -30,10 +24,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept { private static final int STATE_REFERENCE = 1; private static final int STATE_LOADED = 2; - /** - * Used when a bean is partially loaded. - */ - private static final byte FLAG_LOADED_PROP = 1; private static final byte FLAG_CHANGED_PROP = 2; private static final byte FLAG_CHANGEDLOADED_PROP = 3; /** @@ -47,43 +37,21 @@ public final class InterceptReadWrite implements EntityBeanIntercept { */ private static final byte FLAG_MUTABLE_HASH_SET = 16; - private final ReentrantLock lock = new ReentrantLock(); private transient NodeUsageCollector nodeUsageCollector; - private transient PersistenceContext persistenceContext; - private transient BeanLoader beanLoader; private transient PreGetterCallback preGetterCallback; - private String ebeanServerName; - private boolean deletedFromCollection; - - /** - * The actual entity bean that 'owns' this intercept. - */ - private final EntityBean owner; private EntityBean embeddedOwner; private int embeddedOwnerIndex; /** - * One of NEW, REF, UPD. + * One of NEW, REF, LOADED. */ private int state; + private boolean deletedFromCollection; private boolean forceUpdate; - private boolean readOnly; private boolean dirty; - /** - * Flag set to disable lazy loading. - */ - private boolean disableLazyLoad; - /** - * Flag set when lazy loading failed due to the underlying bean being deleted in the DB. - */ - private boolean lazyLoadFailure; - private boolean fullyLoadedBean; private boolean loadedFromCache; - private final byte[] flags; private Object[] origValues; private Exception[] loadErrors; - private int lazyLoadProperty = -1; - private Object ownerId; private int sortOrder; /** @@ -101,49 +69,14 @@ public final class InterceptReadWrite implements EntityBeanIntercept { * Create with a given entity. */ public InterceptReadWrite(Object ownerBean) { - this.owner = (EntityBean) ownerBean; - this.flags = new byte[owner._ebean_getPropertyNames().length]; + super(ownerBean); } /** * EXPERIMENTAL - Constructor only for use by serialization frameworks. */ public InterceptReadWrite() { - this.owner = null; - this.flags = null; - } - - @Override - public String toString() { - return "InterceptReadWrite@" + hashCode() + "{state=" + state + - (dirty ? " dirty;" : "") + - (forceUpdate ? " forceUpdate;" : "") + - (readOnly ? " readOnly;" : "") + - (disableLazyLoad ? " disableLazyLoad;" : "") + - (lazyLoadFailure ? " lazyLoadFailure;" : "") + - (fullyLoadedBean ? " fullyLoadedBean;" : "") + - (loadedFromCache ? " loadedFromCache;" : "") + - ", pc=" + System.identityHashCode(persistenceContext) + - ", flags=" + Arrays.toString(flags) + - (lazyLoadProperty > -1 ? (", lazyLoadProperty=" + lazyLoadProperty) : "") + - ", loader=" + beanLoader + - (ownerId != null ? (", ownerId=" + ownerId) : "") + - '}'; - } - - @Override - public EntityBean owner() { - return owner; - } - - @Override - public PersistenceContext persistenceContext() { - return persistenceContext; - } - - @Override - public void setPersistenceContext(PersistenceContext persistenceContext) { - this.persistenceContext = persistenceContext; + super(); } @Override @@ -151,16 +84,6 @@ public void setNodeUsageCollector(NodeUsageCollector usageCollector) { this.nodeUsageCollector = usageCollector; } - @Override - public Object ownerId() { - return ownerId; - } - - @Override - public void setOwnerId(Object ownerId) { - this.ownerId = ownerId; - } - @Override public Object embeddedOwner() { return embeddedOwner; @@ -187,39 +110,6 @@ public void setEmbeddedOwner(EntityBean parentBean, int embeddedOwnerIndex) { this.embeddedOwnerIndex = embeddedOwnerIndex; } - @Override - public void setBeanLoader(BeanLoader beanLoader, PersistenceContext ctx) { - this.beanLoader = beanLoader; - this.persistenceContext = ctx; - this.ebeanServerName = beanLoader.name(); - } - - @Override - public void setBeanLoader(BeanLoader beanLoader) { - this.beanLoader = beanLoader; - this.ebeanServerName = beanLoader.name(); - } - - @Override - public boolean isFullyLoadedBean() { - return fullyLoadedBean; - } - - @Override - public void setFullyLoadedBean(boolean fullyLoadedBean) { - this.fullyLoadedBean = fullyLoadedBean; - } - - @Override - public boolean isPartial() { - for (byte flag : flags) { - if ((flag & FLAG_LOADED_PROP) == 0) { - return true; - } - } - return false; - } - @Override public boolean isDirty() { if (dirty) { @@ -257,18 +147,6 @@ public boolean isNewOrDirty() { return isNew() || isDirty(); } - @Override - public boolean hasIdOnly(int idIndex) { - for (int i = 0; i < flags.length; i++) { - if (i == idIndex) { - if ((flags[i] & FLAG_LOADED_PROP) == 0) return false; - } else if ((flags[i] & FLAG_LOADED_PROP) != 0) { - return false; - } - } - return true; - } - @Override public boolean isReference() { return state == STATE_REFERENCE; @@ -298,16 +176,6 @@ public boolean isLoadedFromCache() { return loadedFromCache; } - @Override - public boolean isReadOnly() { - return readOnly; - } - - @Override - public void setReadOnly(boolean readOnly) { - this.readOnly = readOnly; - } - @Override public void setForceUpdate(boolean forceUpdate) { this.forceUpdate = forceUpdate; @@ -356,34 +224,6 @@ public void setLoadedLazy() { this.lazyLoadProperty = -1; } - @Override - public void setLazyLoadFailure(Object ownerId) { - this.lazyLoadFailure = true; - this.ownerId = ownerId; - } - - @Override - public boolean isLazyLoadFailure() { - return lazyLoadFailure; - } - - @Override - public boolean isDisableLazyLoad() { - return disableLazyLoad; - } - - @Override - public void setDisableLazyLoad(boolean disableLazyLoad) { - this.disableLazyLoad = disableLazyLoad; - } - - @Override - public void setEmbeddedLoaded(Object embeddedBean) { - if (embeddedBean instanceof EntityBean) { - ((EntityBean) embeddedBean)._ebean_getIntercept().setLoaded(); - } - } - @Override public boolean isEmbeddedNewOrDirty(Object embeddedBean) { if (embeddedBean == null) { @@ -411,65 +251,6 @@ public Object origValue(int propertyIndex) { return origValues[propertyIndex]; } - @Override - public int findProperty(String propertyName) { - final String[] names = owner._ebean_getPropertyNames(); - for (int i = 0; i < names.length; i++) { - if (names[i].equals(propertyName)) { - return i; - } - } - return -1; - } - - @Override - public String property(int propertyIndex) { - if (propertyIndex == -1) { - return null; - } - return owner._ebean_getPropertyName(propertyIndex); - } - - @Override - public int propertyLength() { - return flags.length; - } - - @Override - public void setPropertyLoaded(String propertyName, boolean loaded) { - final int position = findProperty(propertyName); - if (position == -1) { - throw new IllegalArgumentException("Property " + propertyName + " not found"); - } - if (loaded) { - flags[position] |= FLAG_LOADED_PROP; - } else { - flags[position] &= ~FLAG_LOADED_PROP; - } - } - - @Override - public void setPropertyUnloaded(int propertyIndex) { - flags[propertyIndex] &= ~FLAG_LOADED_PROP; - } - - @Override - public void setLoadedProperty(int propertyIndex) { - flags[propertyIndex] |= FLAG_LOADED_PROP; - } - - @Override - public void setLoadedPropertyAll() { - for (int i = 0; i < flags.length; i++) { - flags[i] |= FLAG_LOADED_PROP; - } - } - - @Override - public boolean isLoadedProperty(int propertyIndex) { - return (flags[propertyIndex] & FLAG_LOADED_PROP) != 0; - } - @Override public boolean isChangedProperty(int propertyIndex) { return (flags[propertyIndex] & FLAG_CHANGED_PROP) != 0; @@ -530,20 +311,6 @@ public void setNewBeanForUpdate() { setDirty(true); } - @Override - public Set loadedPropertyNames() { - if (fullyLoadedBean) { - return null; - } - final Set props = new LinkedHashSet<>(); - for (int i = 0; i < flags.length; i++) { - if ((flags[i] & FLAG_LOADED_PROP) != 0) { - props.add(property(i)); - } - } - return props; - } - @Override public boolean[] dirtyProperties() { final int len = propertyLength(); @@ -668,86 +435,6 @@ public void addDirtyPropertyKey(StringBuilder sb) { } } - @Override - public StringBuilder loadedPropertyKey() { - final StringBuilder sb = new StringBuilder(); - final int len = propertyLength(); - for (int i = 0; i < len; i++) { - if (isLoadedProperty(i)) { - sb.append(i).append(','); - } - } - return sb; - } - - @Override - public boolean[] loaded() { - final boolean[] ret = new boolean[flags.length]; - for (int i = 0; i < ret.length; i++) { - ret[i] = (flags[i] & FLAG_LOADED_PROP) != 0; - } - return ret; - } - - @Override - public int lazyLoadPropertyIndex() { - return lazyLoadProperty; - } - - @Override - public String lazyLoadProperty() { - return property(lazyLoadProperty); - } - - @Override - public void loadBean(int loadProperty) { - lock.lock(); - try { - if (beanLoader == null) { - final Database database = DB.byName(ebeanServerName); - if (database == null) { - throw new PersistenceException(ebeanServerName == null ? "No registered default server" : "Database [" + ebeanServerName + "] is not registered"); - } - // For stand alone reference bean or after deserialisation lazy load - // using the ebeanServer. Synchronise only on the bean. - loadBeanInternal(loadProperty, database.pluginApi().beanLoader()); - return; - } - } finally { - lock.unlock(); - } - final Lock lock = beanLoader.lock(); - try { - // Lazy loading using LoadBeanContext which supports batch loading - // Synchronise on the beanLoader (a 'node' of the LoadBeanContext 'tree') - loadBeanInternal(loadProperty, beanLoader); - } finally { - lock.unlock(); - } - } - - @Override - public void loadBeanInternal(int loadProperty, BeanLoader loader) { - if ((flags[loadProperty] & FLAG_LOADED_PROP) != 0) { - // race condition where multiple threads calling preGetter concurrently - return; - } - if (lazyLoadFailure) { - // failed when batch lazy loaded by another bean in the batch - throw new EntityNotFoundException("(Lazy) loading failed on type:" + owner.getClass().getName() + " id:" + ownerId + " - Bean has been deleted. BeanLoader: " + beanLoader); - } - if (lazyLoadProperty == -1) { - lazyLoadProperty = loadProperty; - loader.loadBean(this); - if (lazyLoadFailure) { - // failed when lazy loading this bean - throw new EntityNotFoundException("Lazy loading failed on type:" + owner.getClass().getName() + " id:" + ownerId + " - Bean has been deleted. BeanLoader: " + beanLoader); - } - // bean should be loaded and intercepting now. setLoaded() has - // been called by the lazy loading mechanism - } - } - /** * Helper method to check if two objects are equal. */ @@ -808,11 +495,6 @@ private static boolean notEqualContent(File file1, File file2) { } } - @Override - public void initialisedMany(int propertyIndex) { - flags[propertyIndex] |= FLAG_LOADED_PROP; - } - @Override public void preGetterCallback(int propertyIndex) { PreGetterCallback preGetterCallback = this.preGetterCallback; @@ -1076,4 +758,16 @@ public String mutableNext(int propertyIndex) { final MutableValueNext next = mutableNext[propertyIndex]; return next != null ? next.content() : null; } + + @Override + public void freeze() { + super.freeze(); + // not allowed to be used for updates + this.dirty = false; + this.forceUpdate = false; + this.deletedFromCollection = false; + this.origValues = null; + this.nodeUsageCollector = null; + this.embeddedOwner = null; + } } diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java index 12d0770d62..7b550d9c42 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanList.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java @@ -44,6 +44,12 @@ public void toString(ToStringBuilder builder) { builder.addCollection(list); } + @Override + public Object freeze() { + // null -> illegal to access reference collection + return list == null ? null : Collections.unmodifiableList(list); + } + @Override public void reset(EntityBean ownerBean, String propertyName) { this.ownerBean = ownerBean; diff --git a/ebean-api/src/main/java/io/ebean/common/BeanMap.java b/ebean-api/src/main/java/io/ebean/common/BeanMap.java index fe959c22c0..03f959e203 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanMap.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanMap.java @@ -37,6 +37,12 @@ public BeanMap(BeanCollectionLoader ebeanServer, EntityBean ownerBean, String pr super(ebeanServer, ownerBean, propertyName); } + @Override + public Object freeze() { + // null -> illegal to access reference collection + return map == null ? null : Collections.unmodifiableMap(map); + } + @Override public void toString(ToStringBuilder builder) { if (map == null || map.isEmpty()) { diff --git a/ebean-api/src/main/java/io/ebean/common/BeanSet.java b/ebean-api/src/main/java/io/ebean/common/BeanSet.java index dda2b24ee9..c58a375f3f 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanSet.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanSet.java @@ -3,10 +3,7 @@ import io.ebean.bean.*; import java.io.Serializable; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; /** * Set capable of lazy loading and modification aware. @@ -43,6 +40,12 @@ public void toString(ToStringBuilder builder) { builder.addCollection(set); } + @Override + public Object freeze() { + // null -> illegal to access reference collection + return set == null ? null : Collections.unmodifiableSet(set); + } + @Override public void reset(EntityBean ownerBean, String propertyName) { this.ownerBean = ownerBean; diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiPersistenceContext.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiPersistenceContext.java index a11f9d2f44..750b356434 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiPersistenceContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiPersistenceContext.java @@ -1,8 +1,11 @@ package io.ebeaninternal.api; +import io.ebean.bean.FrozenBeans; +import io.ebean.bean.EntityBean; import io.ebean.bean.PersistenceContext; import java.util.List; +import java.util.Map; /** * SPI extension to PersistenceContext. @@ -14,4 +17,14 @@ public interface SpiPersistenceContext extends PersistenceContext { */ List dirtyBeans(SpiBeanTypeManager manager); + /** + * Return all the entities from this persistence context. + */ + Map, List> detach(); + + /** + * Attach all the cached entities into this persistence context. + */ + void attach(FrozenBeans cachedBeans); + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java index 28a29cb594..6087383a5a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java @@ -3,6 +3,7 @@ import io.ebean.ProfileLocation; import io.ebean.TransactionCallback; import io.ebean.annotation.DocStoreMode; +import io.ebean.bean.FrozenBeans; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; import io.ebeaninternal.server.core.PersistDeferredRelationship; @@ -223,6 +224,16 @@ public void register(TransactionCallback callback) { transaction.register(callback); } + @Override + public FrozenBeans freezeAndDetach() { + return transaction.freezeAndDetach(); + } + + @Override + public void attach(FrozenBeans frozenBeans) { + transaction.attach(frozenBeans); + } + @Override public boolean isReadOnly() { return transaction.isReadOnly(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareFlag.java b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareFlag.java index 08cdb273eb..1b9d13bf38 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareFlag.java +++ b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareFlag.java @@ -23,4 +23,8 @@ public void setMarkedDirty(boolean markedDirty) { this.markedDirty = markedDirty; } + @Override + public Object freeze() { + throw new UnsupportedOperationException(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareList.java b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareList.java index 4743cf27b3..0977042022 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareList.java +++ b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareList.java @@ -3,12 +3,7 @@ import io.ebean.ModifyAwareType; import java.io.Serializable; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.ListIterator; -import java.util.Objects; +import java.util.*; /** * Modify aware wrapper of a list. @@ -52,6 +47,11 @@ public int hashCode() { return list.hashCode(); } + @Override + public List freeze() { + return Collections.unmodifiableList(list); + } + @Override public boolean isMarkedDirty() { return owner.isMarkedDirty(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareMap.java b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareMap.java index 2b2610a2e7..3f96979050 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareMap.java @@ -3,11 +3,7 @@ import io.ebean.ModifyAwareType; import java.io.Serializable; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; /** * Map that is wraps an underlying map for the purpose of detecting changes. @@ -51,6 +47,11 @@ public int hashCode() { return map.hashCode(); } + @Override + public Map freeze() { + return Collections.unmodifiableMap(map); + } + @Override public boolean isMarkedDirty() { return owner.isMarkedDirty(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareSet.java b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareSet.java index 06353dd988..11d9fc5d8e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareSet.java +++ b/ebean-core/src/main/java/io/ebeaninternal/json/ModifyAwareSet.java @@ -3,10 +3,7 @@ import io.ebean.ModifyAwareType; import java.io.Serializable; -import java.util.Collection; -import java.util.Iterator; -import java.util.Objects; -import java.util.Set; +import java.util.*; /** * Wraps a Set for the purposes of detecting modifications. @@ -33,6 +30,11 @@ public ModifyAwareSet(ModifyAwareType owner, Set underlying) { this.set = underlying; } + @Override + public Set freeze() { + return Collections.unmodifiableSet(set); + } + @Override public boolean isMarkedDirty() { return owner.isMarkedDirty(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index 081ae5d77d..7e62534563 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -687,6 +687,16 @@ void initInheritInfo() { } } + public void freeze(EntityBean entityBean) { + for (BeanProperty beanProperty : propertiesMutable) { + beanProperty.freeze(entityBean); + } + for (BeanPropertyAssocMany many : propertiesMany) { + many.freeze(entityBean); + } + entityBean._ebean_getIntercept().freeze(); + } + public void metricPersistBatch(PersistRequest.Type type, long startNanos, int size) { iudMetrics.addBatch(type, startNanos, size); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 4cac21ef22..9bc5a3d15c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -1,11 +1,9 @@ package io.ebeaninternal.server.deploy; import com.fasterxml.jackson.core.JsonToken; +import io.ebean.ModifyAwareType; import io.ebean.ValuePair; -import io.ebean.bean.EntityBean; -import io.ebean.bean.EntityBeanIntercept; -import io.ebean.bean.MutableValueInfo; -import io.ebean.bean.PersistenceContext; +import io.ebean.bean.*; import io.ebean.config.EncryptKey; import io.ebean.config.dbplatform.DbEncryptFunction; import io.ebean.config.dbplatform.DbPlatformType; @@ -1498,4 +1496,15 @@ public void registerColumn(BeanDescriptor desc, String prefix) { desc.registerColumn(dbColumn, path); } } + + /** + * Freeze mutable types (like DbArray). + */ + public void freeze(EntityBean entityBean) { + Object value = getValue(entityBean); + if (value instanceof ModifyAwareType) { + ModifyAwareType bc = (ModifyAwareType) value; + setValue(entityBean, bc.freeze()); + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 1c2f3aa9bb..17df1462d1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -1133,4 +1133,17 @@ public boolean createJoinTable() { return false; } } + + @Override + public void freeze(EntityBean entityBean) { + Object value = getValue(entityBean); + if (value instanceof BeanCollection) { + BeanCollection bc = (BeanCollection) value; + setValue(entityBean, bc.freeze()); + if (bc.isReference()) { + // make it an error to access the collection (no lazy loading allowed) + entityBean._ebean_getIntercept().setPropertyUnloaded(propertyIndex); + } + } + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java index 3575d8bbbb..c48936f92e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java @@ -31,7 +31,7 @@ class SqlTreeLoadBean implements SqlTreeLoad { final boolean readId; private final boolean readIdNormal; private final boolean disableLazyLoad; - private final boolean readOnlyNoIntercept; + private final boolean readOnly; private final InheritInfo inheritInfo; final String prefix; private final Map pathMap; @@ -54,7 +54,7 @@ class SqlTreeLoadBean implements SqlTreeLoad { this.readId = node.readId; this.readIdNormal = readId && !temporalVersions; this.disableLazyLoad = node.disableLazyLoad; - this.readOnlyNoIntercept = disableLazyLoad && node.readOnly; + this.readOnly = node.readOnly; this.partialObject = node.partialObject; this.properties = node.properties; this.pathMap = node.pathMap; @@ -162,7 +162,7 @@ private void initLazyParent() throws SQLException { void initBeanType() throws SQLException { localDesc = desc; - localBean = desc.createEntityBean2(readOnlyNoIntercept); + localBean = desc.createEntityBean2(readOnly); localIdBinder = idBinder; } @@ -302,7 +302,7 @@ private void createListProxies() { boolean forceNewReference = queryMode == Mode.REFRESH_BEAN; for (STreePropertyAssocMany many : localDesc.propsMany()) { if (many != loadingChildProperty) { - if (readOnlyNoIntercept) { + if (disableLazyLoad && readOnly) { many.createEmptyReference(localBean); } else { // create a proxy for the many (deferred fetching) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DFrozenBeans.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DFrozenBeans.java new file mode 100644 index 0000000000..01bb0b2d68 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/DFrozenBeans.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.transaction; + +import io.ebean.bean.FrozenBeans; +import io.ebean.bean.EntityBean; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +final class DFrozenBeans implements FrozenBeans { + + private static final long serialVersionUID = 1L; + + private final Map, Map> beans; + + DFrozenBeans(Map, Map> beans) { + this.beans = Collections.unmodifiableMap(beans); + } + + Set, Map>> entries() { + return beans.entrySet(); + } +} 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 3ede05de11..2b2a4825d1 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,5 +1,6 @@ package io.ebeaninternal.server.transaction; +import io.ebean.bean.FrozenBeans; import io.ebean.bean.EntityBean; import io.ebeaninternal.api.SpiBeanType; import io.ebeaninternal.api.SpiBeanTypeManager; @@ -44,6 +45,33 @@ public final class DefaultPersistenceContext implements SpiPersistenceContext { public DefaultPersistenceContext() { } + public void attach(FrozenBeans other) { + lock.lock(); + try { + DFrozenBeans otherContext = (DFrozenBeans) other; + for (Map.Entry, Map> entry : otherContext.entries()) { + classContext(entry.getKey()).attach(entry.getValue()); + } + } finally { + lock.unlock(); + } + } + + @Override + public Map, List> detach() { + lock.lock(); + try { + expungeStaleEntries(); + Map, List> result = new LinkedHashMap<>(); + for (Map.Entry, ClassContext> entry : typeCache.entrySet()) { + result.put(entry.getKey(), entry.getValue().beans()); + } + return result; + } finally { + lock.unlock(); + } + } + @Override public void beginIterate() { lock.lock(); @@ -247,6 +275,23 @@ public String toString() { return "size:" + map.size() + " (" + weakCount + " weak)"; } + List beans() { + List beans = new ArrayList<>(map.size()); + for (Object value : map.values()) { + if (value instanceof BeanRef) { + value = ((BeanRef) value).get(); + } + if (value != null) { + beans.add((EntityBean) value); + } + } + return beans; + } + + void attach(Map value) { + map.putAll(value); + } + private Object get(Object id) { Object ret = map.get(id); if (ret instanceof BeanRef) { 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 9aac861c2a..c47efd9895 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 @@ -3,6 +3,7 @@ import io.ebean.ProfileLocation; import io.ebean.TransactionCallback; import io.ebean.annotation.DocStoreMode; +import io.ebean.bean.FrozenBeans; import io.ebean.config.DatabaseConfig; import io.ebean.event.changelog.BeanChange; import io.ebean.event.changelog.ChangeSet; @@ -130,6 +131,16 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { } } + @Override + public FrozenBeans freezeAndDetach() { + return manager.freezeAndDetach(persistenceContext); + } + + @Override + public void attach(FrozenBeans frozenBeans) { + persistenceContext.attach(frozenBeans); + } + @Override public final void setLabel(String label) { this.label = label; 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 c30bfcb225..28722f3b6d 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 @@ -6,6 +6,8 @@ import io.ebean.TxScope; import io.ebean.annotation.PersistBatch; import io.ebean.annotation.TxType; +import io.ebean.bean.FrozenBeans; +import io.ebean.bean.EntityBean; import io.ebean.cache.ServerCacheNotification; import io.ebean.cache.ServerCacheNotify; import io.ebean.config.CurrentTenantProvider; @@ -22,6 +24,7 @@ import io.ebeaninternal.api.TransactionEventTable.TableIUD; import io.ebeaninternal.server.cache.CacheChangeSet; import io.ebeaninternal.server.cluster.ClusterManager; +import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanDescriptorManager; import io.ebeaninternal.server.profile.TimedProfileLocation; import io.ebeaninternal.server.profile.TimedProfileLocationRegistry; @@ -33,7 +36,9 @@ import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -637,4 +642,21 @@ public final void flushTransparent(SpiPersistenceContext persistenceContext, Spi } } + public FrozenBeans freezeAndDetach(SpiPersistenceContext persistenceContext) { + Map, Map> allBeans = new HashMap<>(); + for (Map.Entry, List> entry : persistenceContext.detach().entrySet()) { + allBeans.put(entry.getKey(), freezeBeanMap(entry)); + } + return new DFrozenBeans(allBeans); + } + + private Map freezeBeanMap(Map.Entry, List> entry) { + BeanDescriptor descriptor = beanDescriptorManager.descriptor(entry.getKey()); + Map beanMap = new HashMap<>(); + for (EntityBean entityBean : entry.getValue()) { + descriptor.freeze(entityBean); + beanMap.put(descriptor.getId(entityBean), entityBean); + } + return beanMap; + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/ModifyAwareType.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/ModifyAwareType.java deleted file mode 100644 index 653e27153d..0000000000 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/ModifyAwareType.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.ebeaninternal.server.type; - -public interface ModifyAwareType { - - boolean isDirty(); -} diff --git a/ebean-test/src/test/java/org/tests/model/json/PlainBeanDirtyAware.java b/ebean-test/src/test/java/org/tests/model/json/PlainBeanDirtyAware.java index 8573cbaa3d..00572f4714 100644 --- a/ebean-test/src/test/java/org/tests/model/json/PlainBeanDirtyAware.java +++ b/ebean-test/src/test/java/org/tests/model/json/PlainBeanDirtyAware.java @@ -35,6 +35,11 @@ public boolean isMarkedDirty() { return markedDirty; } + @Override + public Object freeze() { + return this; + } + @Override public void setMarkedDirty(boolean markedDirty) { this.markedDirty = markedDirty; diff --git a/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java b/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java index fb7c002d6c..e53f5707e5 100644 --- a/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java +++ b/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java @@ -1,5 +1,7 @@ package org.tests.query; +import io.ebean.Transaction; +import io.ebean.bean.FrozenBeans; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.InterceptReadOnly; @@ -7,23 +9,150 @@ import io.ebean.DB; import io.ebean.Query; import org.junit.jupiter.api.Test; -import org.tests.model.basic.Customer; -import org.tests.model.basic.ResetBasicData; +import org.tests.model.basic.*; +import java.io.*; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; public class TestQueryOrderById extends BaseTestCase { + + private Set cachedCustomerIds; + + @Test + void cachedBeanContext_attach() { + ResetBasicData.reset(); + + FrozenBeans cachedBeanContext = testSerialiseCachedBeans(buildCachedBeans()); + + testSerialiseCustomer(cachedCustomers.get(0)); + + try (Transaction txn = DB.beginTransaction()) { + + txn.attach(cachedBeanContext); + + List orders = DB.find(Order.class) + .findList(); + + for (Order order : orders) { + Customer customer = order.getCustomer(); + if (cachedCustomerIds.contains(customer.getId())) { + // Illegal to access customer.orders as that was not loaded and no lazy loading is allowed + // List orders1 = customer.getOrders(); + // assertThat(orders1).isEmpty(); + // assertThat(orders1).isSameAs(Collections.EMPTY_LIST); + + List contacts = customer.getContacts(); + assertThat(contacts).isNotNull(); + + // customer.setContacts(new ArrayList<>()); + // customer.setName("modified"); + Address billingAddress = customer.getBillingAddress(); + if (billingAddress != null) { + String line1 = billingAddress.getLine1(); + Country country = billingAddress.getCountry(); + } + Address shippingAddress = customer.getShippingAddress(); +// if (shippingAddress != null) { +// // shippingAddress.getCountry(); +// } + } + } + } + } + + private static FrozenBeans testSerialiseCachedBeans(FrozenBeans source) { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(os); + oos.writeObject(source); + oos.close(); + + byte[] byteArray = os.toByteArray(); + + var is = new ByteArrayInputStream(byteArray); + ObjectInputStream ois = new ObjectInputStream(is); + FrozenBeans result = (FrozenBeans)ois.readObject(); + assertThat(result).isNotNull(); + + return result; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void testSerialiseCustomer(Customer source) { + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(os); + oos.writeObject(source); + oos.close(); + + byte[] byteArray = os.toByteArray(); + + var is = new ByteArrayInputStream(byteArray); + ObjectInputStream ois = new ObjectInputStream(is); + Customer customer = (Customer)ois.readObject(); + + assertThat(customer.getId()).isEqualTo(source.getId()); + assertThat(customer.getName()).isEqualTo(source.getName()); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private List cachedCustomers; + + private FrozenBeans buildCachedBeans() { + try (Transaction txn = DB.beginTransaction()) { + + List list = DB.find(Customer.class) + .setReadOnly(true) + //.setDisableLazyLoading(true) + .orderBy("id") + .setMaxRows(3) + .findList(); + + // perform some lazy loading if we desire + for (Customer customer : list) { + customer.getContacts().size(); + Address billingAddress = customer.getBillingAddress(); + if (billingAddress != null) { + Country country = billingAddress.getCountry(); + if (country != null) { + country.getName(); + } + } + } + + // can selectively use DisableLazyLoad which is "lazy loading is NO-operation" + //for (Customer customer : list) { + // DB.beanState(customer).setDisableLazyLoad(true); + //} + + cachedCustomerIds = list.stream() + .map(BasicDomain::getId) + .collect(Collectors.toSet()); + + cachedCustomers = list; + return txn.freezeAndDetach(); + } + } + @Test public void orderById_default_expectNotOrderById() { ResetBasicData.reset(); Query query = DB.find(Customer.class) .select("id,name") - .order("id") + .orderBy("id") .setFirstRow(1) .setMaxRows(5);