diff --git a/src/main/java/io/ebean/CacheMode.java b/src/main/java/io/ebean/CacheMode.java new file mode 100644 index 000000000..c55893c0b --- /dev/null +++ b/src/main/java/io/ebean/CacheMode.java @@ -0,0 +1,57 @@ +package io.ebean; +/** + * Enum to control the different cache modes for queryCache (and maybe later) beanCache. + * + * If cache is enabled, you must be careful, what you do with the returned collection. + * By default the returned collections are read only and you will get an exception if you try + * to change them. + * If you add ".setReadOnly(false)" to your query, you'll get a collection that is a clone from the + * one in the cache. That means, changing does not affect the cache + * + * @author Roland Praml, FOCONIS AG + */ +public enum CacheMode { + /** + * Do not use cache. + */ + OFF(false, false), + + /** + * Use the cace (query & store the resut). + */ + ON(true, true), + + /** + * Do not read from cache, but write retrived value to cache. + * Use this, if you want to get the fresh value from database and a CacheMode.ON query will follow. + */ + RECACHE(false, true), + + /** + * Query the cache for value. If it is there, use it, otherwise hit database but do NOT put the value + * into the cache. (this mode is for completeness. There's probably no use case for this) + */ + QUERY_ONLY(true,false); + + private boolean get; + private boolean put; + + private CacheMode(boolean get, boolean put) { + this.get = get; + this.put = put; + } + + /** + * Retruns true if value is read from cache. + */ + public boolean isGet() { + return get; + } + + /** + * Returns true if value (from database) is written to cache. + */ + public boolean isPut() { + return put; + } +} diff --git a/src/main/java/io/ebean/ExpressionList.java b/src/main/java/io/ebean/ExpressionList.java index fd013b4a0..e55d6351d 100644 --- a/src/main/java/io/ebean/ExpressionList.java +++ b/src/main/java/io/ebean/ExpressionList.java @@ -445,12 +445,21 @@ public interface ExpressionList { Query setUseCache(boolean useCache); /** - * Set to true to use the query for executing this query. + * Set the {@link CacheMode} to use the query for executing this query. * * @see Query#setUseQueryCache(boolean) */ - Query setUseQueryCache(boolean useCache); + Query setUseQueryCache(CacheMode useCache); + /** + * Calls {@link #setUseQueryCache(CacheMode)} with ON or OFF. + * + * @see Query#setUseQueryCache(CacheMode) + */ + default Query setUseQueryCache(boolean enabled) { + return setUseQueryCache(enabled ? CacheMode.ON : CacheMode.OFF); + } + /** * Set to true if this query should execute against the doc store. *

diff --git a/src/main/java/io/ebean/Query.java b/src/main/java/io/ebean/Query.java index 6a9c7ddaf..9d383ed8d 100644 --- a/src/main/java/io/ebean/Query.java +++ b/src/main/java/io/ebean/Query.java @@ -1325,10 +1325,17 @@ public interface Query { Query setUseCache(boolean useCache); /** - * Set this to true to use the query cache. + * Set the {@link CacheMode} to use the query for executing this query. */ - Query setUseQueryCache(boolean useQueryCache); - + Query setUseQueryCache(CacheMode useQueryCache); + + /** + * Calls {@link #setUseQueryCache(CacheMode)} with ON or OFF. + */ + default Query setUseQueryCache(boolean enabled) { + return setUseQueryCache(enabled ? CacheMode.ON : CacheMode.OFF); + } + /** * Set to true if this query should execute against the doc store. *

diff --git a/src/main/java/io/ebean/bean/BeanCollection.java b/src/main/java/io/ebean/bean/BeanCollection.java index 31d546407..25d84de7d 100644 --- a/src/main/java/io/ebean/bean/BeanCollection.java +++ b/src/main/java/io/ebean/bean/BeanCollection.java @@ -226,4 +226,9 @@ public interface BeanCollection extends Serializable { * additions and removals have been processed. */ void modifyReset(); + + /** + * Return a shallow copy of this collection that is modifiable. + */ + BeanCollection getShallowCopy(); } diff --git a/src/main/java/io/ebean/common/AbstractBeanCollection.java b/src/main/java/io/ebean/common/AbstractBeanCollection.java index 456993b6d..e13a6dc1d 100644 --- a/src/main/java/io/ebean/common/AbstractBeanCollection.java +++ b/src/main/java/io/ebean/common/AbstractBeanCollection.java @@ -213,4 +213,16 @@ abstract class AbstractBeanCollection implements BeanCollection { boolean holdsModifications() { return modifyHolder != null && modifyHolder.hasModifications(); } + + /** + * Copies all relevant properties for a clone. See {@link #getShallowCopy()} + * @param other + */ + protected void setFromOriginal(AbstractBeanCollection other) { + this.disableLazyLoad = other.disableLazyLoad; + this.ebeanServerName = other.ebeanServerName; + this.loader = other.loader; + this.ownerBean = other.ownerBean; + this.propertyName = other.propertyName; + } } diff --git a/src/main/java/io/ebean/common/BeanList.java b/src/main/java/io/ebean/common/BeanList.java index c11ed6179..42d67b993 100644 --- a/src/main/java/io/ebean/common/BeanList.java +++ b/src/main/java/io/ebean/common/BeanList.java @@ -541,4 +541,11 @@ public final class BeanList extends AbstractBeanCollection implements List } } + + @Override + public BeanCollection getShallowCopy() { + BeanList copy = new BeanList<>(new CopyOnFirstWriteList<>(list)); + copy.setFromOriginal(this); + return copy; + } } diff --git a/src/main/java/io/ebean/common/BeanMap.java b/src/main/java/io/ebean/common/BeanMap.java index 0d049df0a..34bb51f9e 100644 --- a/src/main/java/io/ebean/common/BeanMap.java +++ b/src/main/java/io/ebean/common/BeanMap.java @@ -340,4 +340,10 @@ public final class BeanMap extends AbstractBeanCollection implements Ma return map.values(); } + @Override + public BeanCollection getShallowCopy() { + BeanMap copy = new BeanMap<>(new LinkedHashMap<>(map)); + copy.setFromOriginal(this); + return copy; + } } diff --git a/src/main/java/io/ebean/common/BeanSet.java b/src/main/java/io/ebean/common/BeanSet.java index 6ad3e0072..c19564247 100644 --- a/src/main/java/io/ebean/common/BeanSet.java +++ b/src/main/java/io/ebean/common/BeanSet.java @@ -377,5 +377,11 @@ public final class BeanSet extends AbstractBeanCollection implements Set getShallowCopy() { + BeanSet copy = new BeanSet<>(new LinkedHashSet<>(set)); + copy.setFromOriginal(this); + return copy; + } } diff --git a/src/main/java/io/ebean/common/CopyOnFirstWriteList.java b/src/main/java/io/ebean/common/CopyOnFirstWriteList.java new file mode 100644 index 000000000..aedb58d30 --- /dev/null +++ b/src/main/java/io/ebean/common/CopyOnFirstWriteList.java @@ -0,0 +1,180 @@ +package io.ebean.common; + +import java.io.Serializable; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + * List that copies itself on first write access. Needed to keep memory footprint low and the ability + * to modify lists from cache. + * + * @author Roland Praml, FOCONIS AG + */ +public final class CopyOnFirstWriteList extends AbstractList implements List, Serializable { + + private static final long serialVersionUID = 1L; + + /** + * The underlying List implementation. + */ + private List list; + + + public CopyOnFirstWriteList(List list) { + super(); + this.list = list; + } + + private volatile boolean copied = false; + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(E e) { + checkCopyOnWrite(); + return list.add(e); + } + + @Override + public boolean remove(Object o) { + checkCopyOnWrite(); + return list.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + checkCopyOnWrite(); + return list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + checkCopyOnWrite(); + return list.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + checkCopyOnWrite(); + return list.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + checkCopyOnWrite(); + return list.retainAll(c); + } + + @Override + public void replaceAll(UnaryOperator operator) { + checkCopyOnWrite(); + list.replaceAll(operator); + } + + @Override + public boolean removeIf(Predicate filter) { + checkCopyOnWrite(); + return list.removeIf(filter); + } + + @Override + public void sort(Comparator c) { + checkCopyOnWrite(); + list.sort(c); + } + + @Override + public void clear() { + if (!copied) { + list = new ArrayList<>(); + copied = true; + } + } + + @Override + public boolean equals(Object o) { + return list.equals(o); + } + + @Override + public int hashCode() { + return list.hashCode(); + } + + @Override + public E get(int index) { + return list.get(index); + } + + @Override + public E set(int index, E element) { + checkCopyOnWrite(); + return list.set(index, element); + } + + @Override + public void add(int index, E element) { + checkCopyOnWrite(); + list.add(index, element); + } + + @Override + public E remove(int index) { + checkCopyOnWrite(); + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + private void checkCopyOnWrite() { + if (!copied) { + synchronized (this) { + if (!copied) { + list = new ArrayList<>(list); + copied = true; + } + } + } + } +} diff --git a/src/main/java/io/ebeaninternal/api/SpiQuery.java b/src/main/java/io/ebeaninternal/api/SpiQuery.java index 63fe3228b..8a4022096 100644 --- a/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -1,5 +1,6 @@ package io.ebeaninternal.api; +import io.ebean.CacheMode; import io.ebean.EbeanServer; import io.ebean.ExpressionList; import io.ebean.OrderBy; @@ -583,9 +584,9 @@ public interface SpiQuery extends Query { boolean isUseBeanCache(); /** - * Return true if this query should use/check the query cache. + * Return the cache mode if this query should use/check the query cache. */ - boolean isUseQueryCache(); + CacheMode getUseQueryCache(); /** * Return true if the beans from this query should be loaded into the bean diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 5bb58bb69..f5100ec3c 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -35,6 +35,7 @@ import io.ebean.bean.ObjectGraphNode; import io.ebean.bean.PersistenceContext; import io.ebean.bean.PersistenceContext.WithOption; import io.ebean.cache.ServerCacheManager; +import io.ebean.common.CopyOnFirstWriteList; import io.ebean.config.CurrentTenantProvider; import io.ebean.config.EncryptKeyManager; import io.ebean.config.ServerConfig; @@ -99,6 +100,7 @@ import javax.persistence.NonUniqueResultException; import javax.persistence.OptimisticLockException; import javax.persistence.PersistenceException; import javax.sql.DataSource; + import java.util.Arrays; import java.util.Collection; import java.util.Iterator; @@ -1332,10 +1334,19 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return findIdsWithCopy(((SpiQuery) query).copy(), t); } + @SuppressWarnings("unchecked") @Override public List findIdsWithCopy(Query query, Transaction t) { SpiOrmQueryRequest request = createQueryRequest(Type.ID_LIST, query, t); + Object result = request.getFromQueryCache(); + if (result != null) { + if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) { + return new CopyOnFirstWriteList<>((List) result); + } else { + return (List) result; + } + } try { request.initTransIfRequired(); return request.findIds(); diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index d083c7fe1..78cbda57b 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -1,11 +1,13 @@ package io.ebeaninternal.server.core; +import io.ebean.CacheMode; import io.ebean.PersistenceContextScope; import io.ebean.QueryIterator; import io.ebean.Version; import io.ebean.bean.BeanCollection; import io.ebean.bean.EntityBean; import io.ebean.bean.PersistenceContext; +import io.ebean.common.CopyOnFirstWriteList; import io.ebean.event.BeanFindController; import io.ebean.event.BeanQueryAdapter; import io.ebean.event.BeanQueryRequest; @@ -32,6 +34,8 @@ import javax.persistence.PersistenceException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -471,12 +475,16 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe @SuppressWarnings("unchecked") public Object getFromQueryCache() { - if (!query.isUseQueryCache()) { + if (query.getUseQueryCache() == CacheMode.OFF) { + return null; + } else { + cacheKey = query.queryHash(); + } + + if (!query.getUseQueryCache().isGet()) { return null; } - cacheKey = query.queryHash(); - Object cached = beanDescriptor.queryCacheGet(cacheKey); if (cached != null && isAuditReads() && readAuditQueryType()) { @@ -491,6 +499,18 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe } } + if (Boolean.FALSE.equals(query.isReadOnly())) { + // return shallow copies if readonly is explicitly set to false + if (cached instanceof BeanCollection) { + cached = ((BeanCollection)cached).getShallowCopy(); + } else if (cached instanceof List) { + cached = new CopyOnFirstWriteList<>((List)cached); + } else if (cached instanceof Set) { + cached = new LinkedHashSet<>((Set)cached); + } else if (cached instanceof Map) { + cached = new LinkedHashMap<>((Map)cached); + } + } return cached; } diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java index 72d789a95..915f874dc 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java @@ -463,7 +463,7 @@ public class DefaultExpressionList implements SpiExpressionList { } @Override - public Query setUseQueryCache(boolean useCache) { + public Query setUseQueryCache(CacheMode useCache) { return query.setUseQueryCache(useCache); } diff --git a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java index 37d1e2510..7ba4a5086 100644 --- a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java @@ -742,7 +742,7 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression } @Override - public Query setUseQueryCache(boolean useCache) { + public Query setUseQueryCache(CacheMode useCache) { return exprList.setUseQueryCache(useCache); } diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 77e24aca5..01d81a454 100644 --- a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -13,7 +13,9 @@ import io.ebeaninternal.server.deploy.BeanDescriptor; import javax.persistence.PersistenceException; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; /** @@ -71,7 +73,7 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { flushJdbcBatchOnQuery(request); int result = queryEngine.findCount(request); - if (request.getQuery().isUseQueryCache()) { + if (request.getQuery().getUseQueryCache().isPut()) { request.putToQueryCache(result); } return result; @@ -81,16 +83,28 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { public List findIds(OrmQueryRequest request) { flushJdbcBatchOnQuery(request); - return queryEngine.findIds(request); + List result = queryEngine.findIds(request); + if (request.getQuery().getUseQueryCache().isPut()) { + result = Collections.unmodifiableList(result); + request.putToQueryCache(result); + if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) { + result = new ArrayList<>(result); + } + } + return result; } @Override public List findSingleAttributeList(OrmQueryRequest request) { flushJdbcBatchOnQuery(request); List result = queryEngine.findSingleAttributeList(request); - if (!result.isEmpty() && request.getQuery().isUseQueryCache()) { + if (!result.isEmpty() && request.getQuery().getUseQueryCache().isPut()) { // load the query result into the query cache + result = Collections.unmodifiableList(result); request.putToQueryCache(result); + if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) { + result = new ArrayList<>(result); + } } return result; } @@ -137,9 +151,13 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { } } - if (!result.isEmpty() && query.isUseQueryCache()) { + if (!result.isEmpty() && query.getUseQueryCache().isPut()) { // load the query result into the query cache + result.setReadOnly(true); request.putToQueryCache(result); + if (Boolean.FALSE.equals(query.isReadOnly())) { + result = result.getShallowCopy(); + } } return result; diff --git a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index f94e9261c..11cd893d0 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.querydefn; +import io.ebean.CacheMode; import io.ebean.EbeanServer; import io.ebean.Expression; import io.ebean.ExpressionFactory; @@ -195,7 +196,7 @@ public class DefaultOrmQuery implements SpiQuery { private boolean excludeBeanCache; - private Boolean useQueryCache; + private CacheMode useQueryCache = CacheMode.OFF; private Boolean readOnly; @@ -1094,9 +1095,13 @@ public class DefaultOrmQuery implements SpiQuery { } @Override - public boolean isUseQueryCache() { + public CacheMode getUseQueryCache() { // not using L2 cache for asDraft() query - return !isAsDraft() && Boolean.TRUE.equals(useQueryCache); + if (isAsDraft()) { + return CacheMode.OFF; + } else { + return useQueryCache; + } } @Override @@ -1106,7 +1111,7 @@ public class DefaultOrmQuery implements SpiQuery { } @Override - public DefaultOrmQuery setUseQueryCache(boolean useQueryCache) { + public DefaultOrmQuery setUseQueryCache(CacheMode useQueryCache) { this.useQueryCache = useQueryCache; return this; } diff --git a/src/test/java/org/tests/cache/TestQueryCache.java b/src/test/java/org/tests/cache/TestQueryCache.java index a947b8e45..d9f2dd5a8 100644 --- a/src/test/java/org/tests/cache/TestQueryCache.java +++ b/src/test/java/org/tests/cache/TestQueryCache.java @@ -1,6 +1,7 @@ package org.tests.cache; import io.ebean.BaseTestCase; +import io.ebean.CacheMode; import io.ebean.Ebean; import io.ebean.bean.BeanCollection; import io.ebean.cache.ServerCache; @@ -105,13 +106,13 @@ public class TestQueryCache extends BaseTestCase { LoggedSqlCollector.start(); int count0 = Ebean.find(EColAB.class) - .setUseQueryCache(true) + .setUseQueryCache(CacheMode.ON) .where() .eq("columnB", "count") .findCount(); int count1 = Ebean.find(EColAB.class) - .setUseQueryCache(true) + .setUseQueryCache(CacheMode.ON) .where() .eq("columnB", "count") .findCount(); @@ -124,7 +125,7 @@ public class TestQueryCache extends BaseTestCase { // and now, ensure that we hit the database LoggedSqlCollector.start(); int count2 = Ebean.find(EColAB.class) - .setUseQueryCache(false) + .setUseQueryCache(CacheMode.OFF) .where() .eq("columnB", "count") .findCount(); @@ -140,13 +141,13 @@ public class TestQueryCache extends BaseTestCase { LoggedSqlCollector.start(); int count0 = Ebean.find(EColAB.class) - .setUseQueryCache(true) + .setUseQueryCache(CacheMode.ON) .where() .eq("columnB", "abc") .findCount(); int count1 = Ebean.find(EColAB.class) - .setUseQueryCache(true) + .setUseQueryCache(CacheMode.ON) .where() .eq("columnB", "def") .findCount(); @@ -157,6 +158,58 @@ public class TestQueryCache extends BaseTestCase { assertThat(sql).hasSize(2); // different queries } + + @Test + public void findCountFirstOnThenRecache() { + + + LoggedSqlCollector.start(); + + int count0 = Ebean.find(EColAB.class) + .setUseQueryCache(CacheMode.ON) + .where() + .eq("columnB", "uvw") + .findCount(); + + int count1 = Ebean.find(EColAB.class) + .setUseQueryCache(CacheMode.RECACHE) + .where() + .eq("columnB", "uvw") + .findCount(); + + List sql = LoggedSqlCollector.stop(); + + assertThat(count0).isEqualTo(count1); + assertThat(sql).hasSize(2); // try recache as second query - it must fetch it + + } + + + @Test + public void findCountFirstRecacheThenOn() { + + + LoggedSqlCollector.start(); + + int count0 = Ebean.find(EColAB.class) + .setUseQueryCache(CacheMode.RECACHE) + .where() + .eq("columnB", "xyz") + .findCount(); + + int count1 = Ebean.find(EColAB.class) + .setUseQueryCache(CacheMode.ON) + .where() + .eq("columnB", "xyz") + .findCount(); + + List sql = LoggedSqlCollector.stop(); + + assertThat(count0).isEqualTo(count1); + assertThat(sql).hasSize(1); // try recache as first query - second "ON" query must fetch it. + + } + @Test @SuppressWarnings("unchecked") public void test() { @@ -170,7 +223,7 @@ public class TestQueryCache extends BaseTestCase { .ilike("name", "Rob").findList(); BeanCollection bc = (BeanCollection) list; - Assert.assertFalse(bc.isReadOnly()); + Assert.assertTrue(bc.isReadOnly()); Assert.assertFalse(bc.isEmpty()); Assert.assertTrue(!list.isEmpty()); Assert.assertTrue(Ebean.getBeanState(list.get(0)).isReadOnly()); @@ -188,19 +241,60 @@ public class TestQueryCache extends BaseTestCase { Assert.assertSame(list, list2B); - // TODO: At this stage setReadOnly(false) does not - // create a shallow copy of the List/Set/Map + -// List list3 = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(false).where() -// .ilike("name", "Rob").findList(); -// -// Assert.assertNotSame(list, list3); -// BeanCollection bc3 = (BeanCollection) list3; -// Assert.assertFalse(bc3.isReadOnly()); -// Assert.assertFalse(bc3.isEmpty()); -// Assert.assertTrue(list3.size() > 0); -// Assert.assertFalse(Ebean.getBeanState(list3.get(0)).isReadOnly()); + List list3 = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(false).where() + .ilike("name", "Rob").findList(); + + Assert.assertNotSame(list, list3); + BeanCollection bc3 = (BeanCollection) list3; + Assert.assertFalse(bc3.isReadOnly()); + Assert.assertFalse(bc3.isEmpty()); + Assert.assertTrue(list3.size() > 0); + // TODO: At this stage setReadOnly(false) does create a shallow copy of the List/Set/Map, but does not + // change the read only state in the entities. + // Assert.assertFalse(Ebean.getBeanState(list3.get(0)).isReadOnly()); } + @Test + public void findIds() { + + new EColAB("03", "someId").save(); + new EColAB("04", "someId").save(); + new EColAB("05", "someId").save(); + + + LoggedSqlCollector.start(); + + List colA_first = Ebean.find(EColAB.class) + .setUseQueryCache(CacheMode.ON) + .where() + .eq("columnB", "someId") + .findIds(); + + List colA_second = Ebean.find(EColAB.class) + .setUseQueryCache(CacheMode.ON) + .where() + .eq("columnB", "someId") + .findIds(); + + List sql = LoggedSqlCollector.stop(); + + assertThat(colA_first).isSameAs(colA_second); + assertThat(colA_first).hasSize(3); + assertThat(sql).hasSize(1); + + // and now, ensure that we hit the database + LoggedSqlCollector.start(); + colA_second = Ebean.find(EColAB.class) + .setUseQueryCache(CacheMode.RECACHE) + .where() + .eq("columnB", "someId") + .findIds(); + sql = LoggedSqlCollector.stop(); + + assertThat(sql).hasSize(1); + } + } diff --git a/src/test/java/org/tests/cache/TestQueryCacheReadOnly.java b/src/test/java/org/tests/cache/TestQueryCacheReadOnly.java new file mode 100644 index 000000000..a8b1bb524 --- /dev/null +++ b/src/test/java/org/tests/cache/TestQueryCacheReadOnly.java @@ -0,0 +1,111 @@ +package org.tests.cache; + +import io.ebean.BaseTestCase; +import io.ebean.CacheMode; +import io.ebean.Ebean; +import io.ebean.EbeanServer; +import io.ebean.Query; + +import org.tests.model.basic.EBasicVer; +import org.junit.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.*; + +public class TestQueryCacheReadOnly extends BaseTestCase { + + @Test + public void testReadOnly() { + + EbeanServer server = Ebean.getServer(null); + EBasicVer account = new EBasicVer("an other junk"); + server.save(account); + + Query baseQuery = server.find(EBasicVer.class).setUseQueryCache(CacheMode.ON); + + List alist = baseQuery.findList(); + assertThat(alist).isNotEmpty(); + assertThatThrownBy(alist::clear).hasMessageContaining("This collection is in ReadOnly mode"); + alist = baseQuery.findList(); + assertThat(alist).isNotEmpty(); + assertThatThrownBy(alist::clear).hasMessageContaining("This collection is in ReadOnly mode"); + + Map amap = baseQuery.setMapKey("name").findMap(); + assertThat(amap).isNotEmpty(); + assertThatThrownBy(amap::clear).hasMessageContaining("This collection is in ReadOnly mode"); + amap = baseQuery.setMapKey("name").findMap(); + assertThat(amap).isNotEmpty(); + assertThatThrownBy(amap::clear).hasMessageContaining("This collection is in ReadOnly mode"); + + Set aset = baseQuery.findSet(); + assertThat(aset).isNotEmpty(); + assertThatThrownBy(aset::clear).hasMessageContaining("This collection is in ReadOnly mode"); + aset = baseQuery.findSet(); + assertThat(aset).isNotEmpty(); + assertThatThrownBy(aset::clear).hasMessageContaining("This collection is in ReadOnly mode"); + + // we will get an unmodifiable collection here + List attributeList = baseQuery.select("name").findSingleAttributeList(); + assertThat(attributeList).isNotEmpty(); + assertThatThrownBy(attributeList::clear).isInstanceOf(UnsupportedOperationException.class); + attributeList = baseQuery.select("name").findSingleAttributeList(); + assertThat(attributeList).isNotEmpty(); + assertThatThrownBy(attributeList::clear).isInstanceOf(UnsupportedOperationException.class); + + List idList = baseQuery.select("name").findIds(); + assertThat(idList).isNotEmpty(); + assertThatThrownBy(idList::clear).isInstanceOf(UnsupportedOperationException.class); + idList = baseQuery.select("name").findIds(); + assertThat(idList).isNotEmpty(); + assertThatThrownBy(idList::clear).isInstanceOf(UnsupportedOperationException.class); + } + + + @Test + public void testNotReadOnly() { + + EbeanServer server = Ebean.getServer(null); + EBasicVer account = new EBasicVer("an other junk"); + server.save(account); + + Query baseQuery = server.find(EBasicVer.class).setUseQueryCache(CacheMode.ON).setReadOnly(false); + + List alist = baseQuery.findList(); + assertThat(alist).isNotEmpty(); + alist.clear(); + alist = baseQuery.findList(); + assertThat(alist).isNotEmpty(); + alist.clear(); + + Map amap = baseQuery.setMapKey("name").findMap(); + assertThat(amap).isNotEmpty(); + amap.clear(); + amap = baseQuery.setMapKey("name").findMap(); + assertThat(amap).isNotEmpty(); + amap.clear(); + + Set aset = baseQuery.findSet(); + assertThat(aset).isNotEmpty(); + aset.clear(); + aset = baseQuery.findSet(); + assertThat(aset).isNotEmpty(); + aset.clear(); + + List attributeList = baseQuery.select("name").findSingleAttributeList(); + assertThat(attributeList).isNotEmpty(); + attributeList.clear(); + attributeList = baseQuery.select("name").findSingleAttributeList(); + assertThat(attributeList).isNotEmpty(); + attributeList.clear(); + + List idList = baseQuery.select("name").findIds(); + assertThat(idList).isNotEmpty(); + idList.clear(); + idList = baseQuery.select("name").findIds(); + assertThat(idList).isNotEmpty(); + idList.clear(); + } +}