From 1fef4f7c11ae95bb9ceb12cb97d82d0eee878430 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 20 Nov 2024 23:01:59 +1300 Subject: [PATCH] Add explicit query.setUnmodifiable(true) [for readOnly=true + disableLazyLoad=true] Although this is ok, makes me think that another option is just to have query.setReadOnly(true) to mean ... readOnly + disableLazyLoad + error reading unloaded property or collection. As in, readOnly true without these extra things does not that good [as in the existing readOnly does not seem very good/safe/useful to use]. --- .../src/main/java/io/ebean/QueryBuilder.java | 5 ++ .../java/io/ebean/bean/BeanCollection.java | 5 ++ .../io/ebean/bean/EntityBeanIntercept.java | 6 -- .../java/io/ebean/bean/InterceptBase.java | 6 -- .../java/io/ebean/bean/InterceptReadOnly.java | 9 ++- .../io/ebean/bean/InterceptReadWrite.java | 6 +- .../main/java/io/ebean/common/BeanList.java | 5 ++ .../main/java/io/ebean/common/BeanMap.java | 5 ++ .../main/java/io/ebean/common/BeanSet.java | 10 +-- .../java/io/ebeaninternal/api/SpiQuery.java | 5 ++ .../server/deploy/BeanDescriptor.java | 8 +-- .../server/deploy/BeanPropertyAssocMany.java | 11 +++ .../server/query/DefaultFetchGroupQuery.java | 5 ++ .../server/query/STreePropertyAssocMany.java | 2 +- .../ebeaninternal/server/query/STreeType.java | 5 ++ .../server/query/SqlTreeBuilder.java | 10 +-- .../server/query/SqlTreeCommon.java | 10 +-- .../server/query/SqlTreeLoadBean.java | 13 ++-- .../server/query/SqlTreeNodeBean.java | 4 +- .../server/querydefn/DefaultOrmQuery.java | 22 ++++++ .../java/io/ebean/typequery/QueryBean.java | 6 ++ .../org/tests/query/TestQueryOrderById.java | 72 ++++++++++++++----- 22 files changed, 164 insertions(+), 66 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/QueryBuilder.java b/ebean-api/src/main/java/io/ebean/QueryBuilder.java index 0b863f981..4a6ae429e 100644 --- a/ebean-api/src/main/java/io/ebean/QueryBuilder.java +++ b/ebean-api/src/main/java/io/ebean/QueryBuilder.java @@ -387,6 +387,11 @@ public interface QueryBuilder, T> extends Que */ SELF setUseDocStore(boolean useDocStore); + /** + * When set to true when you want the returned beans to be read only. + */ + SELF setUnmodifiable(boolean unmodifiable); + /** * When set to true when you want the returned beans to be read only. */ 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 1161476dd..99bcf2049 100644 --- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java @@ -240,4 +240,9 @@ public interface BeanCollection extends Serializable, ToStringAware { * Return a shallow copy of this collection that is modifiable. */ BeanCollection shallowCopy(); + + /** + * Freeze the collection returning an unmodifiable version. + */ + Object freeze(); } 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 28463b208..1127c02b2 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -29,12 +29,6 @@ public interface EntityBeanIntercept extends Serializable { */ void setPersistenceContext(PersistenceContext persistenceContext); - /** - * When accessing a property that is not loaded, throw a IllegalStateException - * when true or invoke lazy loading. - */ - void errorOnLazyLoad(boolean lazyLoadAsError); - /** * Turn on profile collection. */ diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java index cad13dbe5..d229e32bc 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java @@ -7,7 +7,6 @@ abstract class InterceptBase implements EntityBeanIntercept { final EntityBean owner; boolean fullyLoadedBean; - boolean errorOnLazyLoad; /** * Create with a given entity. @@ -28,11 +27,6 @@ abstract class InterceptBase implements EntityBeanIntercept { return owner; } - @Override - public final void errorOnLazyLoad(boolean errorOnLazyLoad) { - this.errorOnLazyLoad = errorOnLazyLoad; - } - @Override public final boolean isFullyLoadedBean() { return fullyLoadedBean; 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 9a5ae98f0..b1791f2a5 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java @@ -8,7 +8,7 @@ import java.util.*; * 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 final class InterceptReadOnly extends InterceptBase { @@ -91,7 +91,7 @@ public final class InterceptReadOnly extends InterceptBase { @Override public boolean isPartial() { for (boolean flag : flags) { - if (flag) { + if (!flag) { return true; } } @@ -412,9 +412,8 @@ public final class InterceptReadOnly extends InterceptBase { @Override public void preGetter(int propertyIndex) { - if (errorOnLazyLoad && !flags[propertyIndex]) { - final String property = property(propertyIndex); - throw new IllegalStateException("Property not loaded: " + property); + if (!flags[propertyIndex]) { + throw new IllegalStateException("Property not loaded: " + property(propertyIndex)); } } 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 f5abc476c..9db3c9e1b 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java @@ -650,10 +650,6 @@ public final class InterceptReadWrite extends InterceptBase { @Override public void loadBean(int loadProperty) { - if (errorOnLazyLoad) { - final String property = property(loadProperty); - throw new IllegalStateException("Lazy loading not allowed for " + property); - } lock.lock(); try { if (beanLoader == null) { @@ -661,7 +657,7 @@ public final class InterceptReadWrite extends InterceptBase { 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 + // For stand-alone reference bean or after deserialisation lazy load // using the ebeanServer. Synchronise only on the bean. loadBeanInternal(loadProperty, database.pluginApi().beanLoader()); return; 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 12d0770d6..d949fa7b3 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanList.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java @@ -39,6 +39,11 @@ public final class BeanList extends AbstractBeanCollection implements List super(loader, ownerBean, propertyName); } + @Override + public List freeze() { + return list == null ? null : Collections.unmodifiableList(list); + } + @Override public void toString(ToStringBuilder builder) { builder.addCollection(list); 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 fe959c22c..81da1bfdc 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,11 @@ public final class BeanMap extends AbstractBeanCollection implements Ma super(ebeanServer, ownerBean, propertyName); } + @Override + public Map freeze() { + 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 dda2b24ee..cca0278b5 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 @@ package io.ebean.common; 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. @@ -38,6 +35,11 @@ public final class BeanSet extends AbstractBeanCollection implements Set freeze() { + return set == null ? null : Collections.unmodifiableSet(set); + } + @Override public void toString(ToStringBuilder builder) { builder.addCollection(set); diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java index 3d1e46086..7854ac39f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -784,6 +784,11 @@ public interface SpiQuery extends Query, SpiQueryFetch, TxnProfileEventCod */ Boolean isReadOnly(); + /** + * Return true if the beans returned by this query should be unmodifiable. + */ + boolean isUnmodifiable(); + /** * Return the query timeout. */ 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 a46dda00e..f6ee08925 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,14 +687,14 @@ public class BeanDescriptor implements BeanType, STreeType, SpiBeanType { } } + @Override public void freeze(EntityBean entityBean) { for (BeanProperty beanProperty : propertiesMutable) { beanProperty.freeze(entityBean); } -// for (BeanPropertyAssocMany many : propertiesMany) { -// many.freeze(entityBean); -// } -// entityBean._ebean_getIntercept().freeze(); + for (BeanPropertyAssocMany many : propertiesMany) { + many.freeze(entityBean); + } } public void metricPersistBatch(PersistRequest.Type type, long startNanos, int size) { 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 cc40fed56..7479ce551 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 @@ -1165,4 +1165,15 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST return false; } } + + @Override + public void freeze(EntityBean entityBean) { + Object value = getValue(entityBean); + if (value instanceof BeanCollection) { + setValue(entityBean, ((BeanCollection) value).freeze()); + } else if (value == null) { + // 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/DefaultFetchGroupQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java index c71dbe8db..4e18b802a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultFetchGroupQuery.java @@ -513,6 +513,11 @@ final class DefaultFetchGroupQuery implements SpiFetchGroupQuery, SpiQuery throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); } + @Override + public Query setUnmodifiable(boolean unmodifiable) { + throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); + } + @Override public Query setReadOnly(boolean readOnly) { throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup"); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocMany.java index a7102af8c..ff0317084 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreePropertyAssocMany.java @@ -34,7 +34,7 @@ public interface STreePropertyAssocMany extends STreePropertyAssoc { BeanCollection createReference(EntityBean localBean, boolean forceNewReference); /** - * Populate the collection for read-only disabled lazy loading (aka Java Collections non mutable empty collection). + * Populate the collection for read-only disabled lazy loading (aka Java Collections non-mutable empty collection). */ void createEmptyReference(EntityBean localBean); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java index 449b65926..76fd064e5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java @@ -99,6 +99,11 @@ public interface STreeType { */ void postLoad(Object localBean); + /** + * Freeze the properties of the entity. + */ + void freeze(EntityBean entityBean); + /** * Return the base table to use given the temporalMode. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 72a31e898..c183c7934 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -45,12 +45,12 @@ public final class SqlTreeBuilder { */ private final boolean rawNoId; private final boolean disableLazyLoad; - private final boolean readOnly; private final SpiQuery.TemporalMode temporalMode; private SqlTreeNode rootNode; private boolean sqlDistinct; private final boolean platformDistinctNoLobs; private final SqlTreeCommon common; + private final boolean unmodifiable; /** * Construct for RawSql query. @@ -60,7 +60,7 @@ public final class SqlTreeBuilder { this.desc = request.descriptor(); this.rawNoId = rawNoId; this.disableLazyLoad = request.query().isDisableLazyLoading(); - this.readOnly = Boolean.TRUE.equals(request.query().isReadOnly()); + this.unmodifiable = request.query().isUnmodifiable(); this.query = null; this.subQuery = false; this.distinctOnPlatform = false; @@ -72,7 +72,7 @@ public final class SqlTreeBuilder { this.manyWhereJoins = null; this.alias = null; this.ctx = null; - this.common = new SqlTreeCommon(temporalMode, disableLazyLoad, readOnly, null); + this.common = new SqlTreeCommon(temporalMode, disableLazyLoad, unmodifiable, null); } /** @@ -87,7 +87,7 @@ public final class SqlTreeBuilder { this.query = request.query(); this.temporalMode = SpiQuery.TemporalMode.of(query); this.disableLazyLoad = query.isDisableLazyLoading(); - this.readOnly = Boolean.TRUE.equals(query.isReadOnly()); + this.unmodifiable = query.isUnmodifiable(); this.subQuery = Type.SQ_EXISTS == query.type() || Type.SQ_EX == query.type() || Type.ID_LIST == query.type() @@ -105,7 +105,7 @@ public final class SqlTreeBuilder { CQueryDraftSupport draftSupport = builder.draftSupport(query); String colAlias = subQuery ? null : columnAliasPrefix; this.ctx = new DefaultDbSqlContext(alias, colAlias, historySupport, draftSupport, fromForUpdate); - this.common = new SqlTreeCommon(temporalMode, disableLazyLoad, readOnly, includeJoin); + this.common = new SqlTreeCommon(temporalMode, disableLazyLoad, unmodifiable, includeJoin); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeCommon.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeCommon.java index 9c60585f8..2121563df 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeCommon.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeCommon.java @@ -7,13 +7,13 @@ final class SqlTreeCommon { private final SpiQuery.TemporalMode temporalMode; private final boolean disableLazyLoad; - private final boolean readOnly; + private final boolean unmodifiable; private final TableJoin includeJoin; - SqlTreeCommon(SpiQuery.TemporalMode temporalMode, boolean disableLazyLoad, boolean readOnly, TableJoin includeJoin) { + SqlTreeCommon(SpiQuery.TemporalMode temporalMode, boolean disableLazyLoad, boolean unmodifiable, TableJoin includeJoin) { this.temporalMode = temporalMode; this.disableLazyLoad = disableLazyLoad; - this.readOnly = readOnly; + this.unmodifiable = unmodifiable; this.includeJoin = includeJoin; } @@ -25,8 +25,8 @@ final class SqlTreeCommon { return disableLazyLoad; } - boolean readOnly() { - return readOnly; + boolean unmodifiable() { + return unmodifiable; } TableJoin includeJoin() { 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 3575d8bbb..ea1987cb4 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 unmodifiable; 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.unmodifiable = node.unmodifiable; this.partialObject = node.partialObject; this.properties = node.properties; this.pathMap = node.pathMap; @@ -162,7 +162,7 @@ class SqlTreeLoadBean implements SqlTreeLoad { void initBeanType() throws SQLException { localDesc = desc; - localBean = desc.createEntityBean2(readOnlyNoIntercept); + localBean = desc.createEntityBean2(unmodifiable); localIdBinder = idBinder; } @@ -280,6 +280,9 @@ class SqlTreeLoadBean implements SqlTreeLoad { if (!partialObject) { ebi.setFullyLoadedBean(true); } + if (unmodifiable) { + localDesc.freeze(localBean); + } } else if (!partialObject) { ebi.setFullyLoadedBean(true); } else if (readId && !usingContextBean) { @@ -302,9 +305,7 @@ class SqlTreeLoadBean implements SqlTreeLoad { boolean forceNewReference = queryMode == Mode.REFRESH_BEAN; for (STreePropertyAssocMany many : localDesc.propsMany()) { if (many != loadingChildProperty) { - if (readOnlyNoIntercept) { - many.createEmptyReference(localBean); - } else { + if (!unmodifiable) { // create a proxy for the many (deferred fetching) BeanCollection ref = many.createReference(localBean, forceNewReference); if (ref != null) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java index e8c11be55..82c8ffc04 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java @@ -38,7 +38,7 @@ class SqlTreeNodeBean implements SqlTreeNode { final boolean readId; final boolean readIdNormal; final boolean disableLazyLoad; - final boolean readOnly; + final boolean unmodifiable; final InheritInfo inheritInfo; final String prefix; final Map pathMap; @@ -92,7 +92,7 @@ class SqlTreeNodeBean implements SqlTreeNode { this.readId = !aggregationRoot && withId && desc.hasId(); this.readIdNormal = readId && !temporalVersions; this.disableLazyLoad = common.disableLazyLoad() || !readIdNormal || desc.isRawSqlBased(); - this.readOnly = common.readOnly(); + this.unmodifiable = common.unmodifiable(); this.partialObject = props.isPartialObject(); this.properties = props.props(); this.children = myChildren == null ? Collections.emptyList() : myChildren; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 0a433669a..e85d9db28 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -133,6 +133,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { private CacheMode useBeanCache = CacheMode.AUTO; private CacheMode useQueryCache = CacheMode.OFF; private Boolean readOnly; + private boolean unmodifiable; private PersistenceContextScope persistenceContextScope; /** @@ -514,6 +515,15 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { if (!useDocStore) { createExtraJoinsToSupportManyWhereClause(); } + if (unmodifiable) { + readOnly = Boolean.TRUE; + disableLazyLoading = true; + persistenceContextScope = PersistenceContextScope.QUERY; + } else if (disableLazyLoading && Boolean.TRUE.equals(readOnly)) { + // "upgrade" this to unmodifiable? hmmm. + unmodifiable = true; + persistenceContextScope = PersistenceContextScope.QUERY; + } return markQueryJoins(); } @@ -763,6 +773,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { copy.nativeSql = nativeSql; copy.useBeanCache = useBeanCache; copy.useQueryCache = useQueryCache; + copy.unmodifiable = unmodifiable; copy.readOnly = readOnly; if (detail != null) { copy.detail = detail.copy(); @@ -1276,11 +1287,22 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { return versionsEnd; } + @Override + public Query setUnmodifiable(boolean unmodifiable) { + this.unmodifiable = unmodifiable; + return this; + } + @Override public final Boolean isReadOnly() { return readOnly; } + @Override + public boolean isUnmodifiable() { + return unmodifiable; + } + @Override public final Query setReadOnly(boolean readOnly) { this.readOnly = readOnly; diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java index 44fbabf30..4c84a367f 100644 --- a/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java +++ b/ebean-querybean/src/main/java/io/ebean/typequery/QueryBean.java @@ -509,6 +509,12 @@ public abstract class QueryBean> implements IQueryB return root; } + @Override + public R setUnmodifiable(boolean unmodifiable) { + query.setUnmodifiable(unmodifiable); + return root; + } + @Override public final R setReadOnly(boolean readOnly) { query.setReadOnly(readOnly); 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 39b0a37b2..270495171 100644 --- a/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java +++ b/ebean-test/src/test/java/org/tests/query/TestQueryOrderById.java @@ -1,26 +1,65 @@ package org.tests.query; -import io.ebean.bean.BeanCollection; +import io.ebean.DB; +import io.ebean.Query; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.InterceptReadOnly; import io.ebean.xtest.BaseTestCase; -import io.ebean.DB; -import io.ebean.Query; import org.junit.jupiter.api.Test; import org.tests.model.basic.Address; import org.tests.model.basic.Contact; import org.tests.model.basic.Customer; import org.tests.model.basic.ResetBasicData; -import java.util.Collections; +import java.io.*; import java.util.List; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class TestQueryOrderById extends BaseTestCase { + @Test + void unmodifiableResult() throws IOException, ClassNotFoundException { + ResetBasicData.reset(); + + List result = DB.find(Customer.class) + .setUnmodifiable(true) + .select("id,name") + .fetch("contacts") + .orderBy("id") + .findList(); + + assertThat(result).isNotEmpty(); + assertThatThrownBy(() -> result.add(new Customer())) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("ReadOnly"); + + Customer customer = result.get(0); + List contacts = customer.getContacts(); + assertThat(contacts).isNotEmpty(); + assertThatThrownBy(() -> contacts.add(new Contact())) + .isInstanceOf(UnsupportedOperationException.class); + + assertThatThrownBy(customer::getOrders) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Property not loaded: orders"); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(os); + + oos.writeObject(customer); + oos.flush(); + oos.close(); + + ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(is); + + Customer read = (Customer) ois.readObject(); + assertThat(read.getName()).isEqualTo(customer.getName()); + } + @Test void immutableResult() { ResetBasicData.reset(); @@ -38,12 +77,8 @@ class TestQueryOrderById extends BaseTestCase { List contacts = result.get(0).getContacts(); assertThat(contacts).isNotEmpty(); - assertThat(contacts).isInstanceOf(BeanCollection.class); - BeanCollection bc = (BeanCollection)contacts; - assertThat(bc.isReadOnly()).isTrue(); assertThatThrownBy(() -> contacts.add(new Contact())) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("ReadOnly"); + .isInstanceOf(UnsupportedOperationException.class); } @Test @@ -70,24 +105,27 @@ class TestQueryOrderById extends BaseTestCase { EntityBeanIntercept intercept = ((EntityBean) customer)._ebean_getIntercept(); int pos = intercept.findProperty("billingAddress"); assertThat(intercept.isLoadedProperty(pos)).isFalse(); - assertThat(customer.getBillingAddress()) + assertThatThrownBy(() -> customer.getBillingAddress()) .describedAs("Not loaded property returns null") - .isNull(); + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Property not loaded: billingAddress"); assertThatThrownBy(() -> customer.setBillingAddress(new Address())) .describedAs("Not allowed to mutate a readOnly bean") .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("ReadOnly"); - - intercept.errorOnLazyLoad(true); + .hasMessageContaining("Property not loaded: billingAddress"); assertThatThrownBy(customer::getBillingAddress) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("Property not loaded: billingAddress"); assertThat(intercept).isInstanceOf(InterceptReadOnly.class); - assertThat(customer.getOrders()).isSameAs(Collections.EMPTY_LIST); - assertThat(customer.getContacts()).isSameAs(Collections.EMPTY_LIST); + assertThatThrownBy(customer::getOrders) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Property not loaded: orders"); + assertThatThrownBy(customer::getContacts) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Property not loaded: contacts"); } @Test