diff --git a/pom.xml b/pom.xml index 76834dc52..6bf819abe 100644 --- a/pom.xml +++ b/pom.xml @@ -92,7 +92,7 @@ io.ebean ebean-annotation - 3.4 + 3.5 diff --git a/src/main/java/io/ebean/CacheMode.java b/src/main/java/io/ebean/CacheMode.java index 5ca2c47b6..6fcaedb00 100644 --- a/src/main/java/io/ebean/CacheMode.java +++ b/src/main/java/io/ebean/CacheMode.java @@ -22,6 +22,11 @@ public enum CacheMode { */ ON(true, true), + /** + * Only used for bean caching. We automatically use the cache for findOne() but not findList(). + */ + AUTO(true, true), + /** * Do not read from cache, but write retrieved value to cache. * Use this, if you want to get the fresh value from database and a CacheMode.ON query will follow. diff --git a/src/main/java/io/ebean/cache/ServerCache.java b/src/main/java/io/ebean/cache/ServerCache.java index 363c61d84..243632d3d 100644 --- a/src/main/java/io/ebean/cache/ServerCache.java +++ b/src/main/java/io/ebean/cache/ServerCache.java @@ -1,5 +1,9 @@ package io.ebean.cache; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + /** * Represents part of the "L2" server side cache. *

@@ -13,6 +17,18 @@ package io.ebean.cache; */ public interface ServerCache { + default Map getAll(Set keys){ + + Map map = new LinkedHashMap<>(); + for (Object key : keys) { + Object value = get(key); + if (value != null) { + map.put(key, value); + } + } + return map; + } + /** * Return the value given the key. */ diff --git a/src/main/java/io/ebeaninternal/api/BeanCacheResult.java b/src/main/java/io/ebeaninternal/api/BeanCacheResult.java new file mode 100644 index 000000000..2196cac3c --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/BeanCacheResult.java @@ -0,0 +1,54 @@ +package io.ebeaninternal.api; + +import java.util.ArrayList; +import java.util.List; + +/** + * The results of bean cache hit. + */ +public class BeanCacheResult { + + private List> list = new ArrayList<>(); + + /** + * Add an entry. + */ + public void add(T bean, Object key) { + list.add(new Entry<>(bean, key)); + } + + /** + * Return the hits. + */ + public List> hits() { + return list; + } + + /** + * Bean and cache key pair. + */ + static class Entry { + + private final T bean; + private final Object key; + + public Entry(T bean, Object key) { + this.bean = bean; + this.key = key; + } + + /** + * Return the natural key or id value. + */ + public Object getKey() { + return key; + } + + /** + * Return the bean. + */ + public T getBean() { + return bean; + } + } +} diff --git a/src/main/java/io/ebeaninternal/api/NaturalKeyEntry.java b/src/main/java/io/ebeaninternal/api/NaturalKeyEntry.java new file mode 100644 index 000000000..a33fa2ead --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/NaturalKeyEntry.java @@ -0,0 +1,66 @@ +package io.ebeaninternal.api; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Natural key entry with name value pairs for each of the properties making up the key. + */ +public class NaturalKeyEntry { + + private Map map = new HashMap<>(); + private Object key; + private Object inValue; + + /** + * Used when query query just has a series of EQ expressions (no IN clause). + */ + public NaturalKeyEntry(String[] naturalKey, List eqList) { + this(naturalKey, eqList, null, null); + } + + /** + * Create when query uses an IN clause. + */ + public NaturalKeyEntry(String[] naturalKey, List eqList, String inProperty, Object inValue) { + for (NaturalKeyEq eq : eqList) { + map.put(eq.property, eq.value); + } + if (inProperty != null) { + map.put(inProperty, inValue); + this.inValue = inValue; + } + this.key = calculateKey(naturalKey); + } + + + private Object calculateKey(String[] naturalKey) { + + if (naturalKey.length == 1) { + return map.get(naturalKey[0]); + } + + StringBuilder sb = new StringBuilder(); + for (String key : naturalKey) { + sb.append(map.get(key)).append(";"); + } + + return sb.toString(); + } + + /** + * Return the natural cache key. + */ + public Object key() { + return key; + } + + /** + * Return the inValue (used to remove from IN clause of original query). + */ + public Object getInValue() { + return inValue; + } +} diff --git a/src/main/java/io/ebeaninternal/api/NaturalKeyEq.java b/src/main/java/io/ebeaninternal/api/NaturalKeyEq.java new file mode 100644 index 000000000..e7a6218df --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/NaturalKeyEq.java @@ -0,0 +1,15 @@ +package io.ebeaninternal.api; + +/** + * A property value pair in a natural key lookup. + */ +public class NaturalKeyEq { + + final String property; + final Object value; + + public NaturalKeyEq(String property, Object value) { + this.property = property; + this.value = value; + } +} diff --git a/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java b/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java new file mode 100644 index 000000000..aa6ad24fc --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java @@ -0,0 +1,170 @@ +package io.ebeaninternal.api; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Collects the data for processing the natural key cache processing. + */ +public class NaturalKeyQueryData { + + private final String[] naturalKey; + + private Collection inValues; + + private String inProperty; + + private List eqList; + + private NaturalKeySet set; + + private int hitCount; + + public NaturalKeyQueryData(String[] naturalKey) { + this.naturalKey = naturalKey; + } + + private boolean matchProperty(String propName) { + + for (String key : naturalKey) { + if (key.equals(propName)) { + return true; + } + } + return false; + } + + /** + * Match for IN expression. We only allow one IN clause. + */ + public boolean matchIn(String propName, Collection sourceValues) { + if (inProperty != null) { + // only 1 IN allowed (to project naturalIds) + return false; + } + if (matchProperty(propName)) { + this.inProperty = propName; + this.inValues = sourceValues; + return true; + } + return false; + } + + /** + * Match for an EQ expression. + */ + public boolean matchEq(String propName, Object bindValue) { + if (matchProperty(propName)) { + if (eqList == null) { + eqList = new ArrayList<>(); + } + eqList.add(new NaturalKeyEq(propName, bindValue)); + return true; + } + return false; + } + + /** + * Build and return the set of natural keys we will use. + */ + public NaturalKeySet buildKeys() { + + if (!expressionCount() || !matchProperties()) { + return null; + } + + this.set = new NaturalKeySet(); + if (inValues == null) { + // only one - a findOne() + set.add(new NaturalKeyEntry(naturalKey, eqList)); + + } else { + // a findList() with an IN clause so we project + // for every IN value a natural key combination + for (Object inValue : inValues) { + set.add(new NaturalKeyEntry(naturalKey, eqList, inProperty, inValue)); + } + } + + return set; + } + + /** + * Return true if the properties match the natural key properties. + */ + private boolean matchProperties() { + if (naturalKey.length == 1) { + // simple single property case + if (inProperty != null) { + return inProperty.equals(naturalKey[0]); + } else { + return eqList.get(0).property.equals(naturalKey[0]); + } + } + + // multiple properties case + Set exprProps = new HashSet<>(); + if (inProperty != null) { + exprProps.add(inProperty); + } + if (eqList != null) { + for (NaturalKeyEq eq : eqList) { + exprProps.add(eq.property); + } + } + if (exprProps.size() != naturalKey.length) { + return false; + } + for (String key : naturalKey) { + if (!exprProps.remove(key)) { + return false; + } + } + + return exprProps.isEmpty(); + } + + /** + * Check that all the natural key properties are defined. + */ + private boolean expressionCount() { + + int defined = (inValues == null) ? 0 : 1; + defined += (eqList == null) ? 0 : eqList.size(); + return defined == naturalKey.length; + } + + /** + * Return the number of entries in the IN clause left remaining (to hit the DB with). + */ + public boolean allHits() { + return hitCount > 0 + && hitCount == set.size() + && (inValues == null || inValues.isEmpty()); + } + + /** + * Adjust the IN clause removing the hit entry. + */ + public List removeHits(BeanCacheResult cacheResult) { + + List> hits = cacheResult.hits(); + this.hitCount = hits.size(); + + List beans = new ArrayList<>(hitCount); + + for (BeanCacheResult.Entry hit : hits) { + if (inValues != null) { + Object naturalKey = hit.getKey(); + Object inValue = set.getInValue(naturalKey); + inValues.remove(inValue); + } + beans.add(hit.getBean()); + } + + return beans; + } +} diff --git a/src/main/java/io/ebeaninternal/api/NaturalKeySet.java b/src/main/java/io/ebeaninternal/api/NaturalKeySet.java new file mode 100644 index 000000000..64ac20aff --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/NaturalKeySet.java @@ -0,0 +1,31 @@ +package io.ebeaninternal.api; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +public class NaturalKeySet { + + + private final Map map = new LinkedHashMap<>(); + + public NaturalKeySet() { + } + + public int size() { + return map.size(); + } + + public void add(NaturalKeyEntry entry) { + map.put(entry.key(), entry); + } + + public Set keys() { + return map.keySet(); + } + + public Object getInValue(Object naturalKey) { + NaturalKeyEntry entry = map.get(naturalKey); + return entry.getInValue(); + } +} diff --git a/src/main/java/io/ebeaninternal/api/SpiExpression.java b/src/main/java/io/ebeaninternal/api/SpiExpression.java index df69ad702..f97380eb1 100644 --- a/src/main/java/io/ebeaninternal/api/SpiExpression.java +++ b/src/main/java/io/ebeaninternal/api/SpiExpression.java @@ -97,4 +97,9 @@ public interface SpiExpression extends Expression { * Return the bind Id value if this is a "equal to" expression for the id property. */ Object getIdEqualTo(String idName); + + /** + * Check for match to a natural key query returning false if it doesn't match. + */ + boolean naturalKey(NaturalKeyQueryData data); } diff --git a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java index 0f8078c9d..4eb6a1284 100644 --- a/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/api/SpiExpressionRequest.java @@ -74,5 +74,5 @@ public interface SpiExpressionRequest { /** * Append IN expression taking into account platform and type support for Multi-value. */ - void appendInExpression(boolean not, Object[] bindValues); + void appendInExpression(boolean not, List bindValues); } diff --git a/src/main/java/io/ebeaninternal/api/SpiQuery.java b/src/main/java/io/ebeaninternal/api/SpiQuery.java index 6ecedb37b..b89544ca1 100644 --- a/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -366,6 +366,17 @@ public interface SpiQuery extends Query, TxnProfileEventCodes { */ ManyWhereJoins getManyWhereJoins(); + /** + * Reset AUTO mode to OFF for findList(). Expect explicit cache use with findList(). + */ + void resetBeanCacheAutoMode(); + + /** + * Collect natural key data for this query or null if the query does not match + * the requirements of natural key lookup. + */ + NaturalKeyQueryData naturalKey(); + /** * Return a Natural Key bind parameter if supported by this query. */ @@ -579,27 +590,30 @@ public interface SpiQuery extends Query, TxnProfileEventCodes { boolean hasMaxRowsOrFirstRow(); /** - * Return true if the bean cache should be exclude for query or lazy loading. + * Return true if the query should GET against bean cache. */ - boolean isExcludeBeanCache(); + boolean isBeanCacheGet(); /** - * Return true if this query should use the bean cache. - * It is not skipped and bean caching is supported. + * Return true if the query should PUT against the bean cache. */ - boolean isUseBeanCache(); + boolean isBeanCachePut(); + + /** + * Return true if the bean cache is being explicitly loaded via RECACHE mode. + */ + boolean isBeanCacheReload(); + + /** + * Return the cache mode for using the bean cache (Get and Put). + */ + CacheMode getUseBeanCache(); /** * Return the cache mode if this query should use/check the query cache. */ CacheMode getUseQueryCache(); - /** - * Return true if the beans from this query should be loaded into the bean - * cache. - */ - boolean isLoadBeanCache(); - /** * Return true if the beans returned by this query should be read only. */ diff --git a/src/main/java/io/ebeaninternal/server/autotune/service/BaseQueryTuner.java b/src/main/java/io/ebeaninternal/server/autotune/service/BaseQueryTuner.java index 7a0743900..1014219e6 100644 --- a/src/main/java/io/ebeaninternal/server/autotune/service/BaseQueryTuner.java +++ b/src/main/java/io/ebeaninternal/server/autotune/service/BaseQueryTuner.java @@ -144,7 +144,7 @@ public class BaseQueryTuner { default: // not using autoTune when explicitly loading the l2 bean cache // or when using Versions query - return !query.isLoadBeanCache() && SpiQuery.TemporalMode.VERSIONS != query.getTemporalMode(); + return !query.isBeanCacheReload() && SpiQuery.TemporalMode.VERSIONS != query.getTemporalMode(); } } diff --git a/src/main/java/io/ebeaninternal/server/core/CacheOptions.java b/src/main/java/io/ebeaninternal/server/core/CacheOptions.java index 751c7fded..5cb7741a2 100644 --- a/src/main/java/io/ebeaninternal/server/core/CacheOptions.java +++ b/src/main/java/io/ebeaninternal/server/core/CacheOptions.java @@ -15,7 +15,7 @@ public class CacheOptions { private final boolean enableBeanCache; private final boolean enableQueryCache; private final boolean readOnly; - private final String naturalKey; + private final String[] naturalKey; /** * Construct for no caching. @@ -30,7 +30,7 @@ public class CacheOptions { /** * Construct with cache annotation. */ - public CacheOptions(Cache cache, String naturalKey) { + public CacheOptions(Cache cache, String[] naturalKey) { enableBeanCache = cache.enableBeanCache(); enableQueryCache = cache.enableQueryCache(); readOnly = cache.readOnly(); @@ -61,7 +61,7 @@ public class CacheOptions { /** * Return the natural key property name. */ - public String getNaturalKey() { + public String[] getNaturalKey() { return naturalKey; } } diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index a54142ac1..4cdef5912 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -1150,7 +1150,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } } - if (!query.isUseBeanCache() || (t != null && t.isSkipCache())) { + if (!query.isBeanCacheGet() || (t != null && t.isSkipCache())) { return null; } @@ -1179,7 +1179,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { SpiQuery spiQuery = (SpiQuery) query; spiQuery.setType(Type.BEAN); - if (SpiQuery.Mode.NORMAL == spiQuery.getMode() && !spiQuery.isLoadBeanCache()) { + if (SpiQuery.Mode.NORMAL == spiQuery.getMode() && !spiQuery.isBeanCacheReload()) { // See if we can skip doing the fetch completely by getting the bean from the // persistence context or the bean cache T bean = findIdCheckPersistenceContextAndCache(t, spiQuery, spiQuery.getId()); @@ -1217,22 +1217,12 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return findId(query, transaction); } - SpiTransaction t = (SpiTransaction) transaction; - if (t == null) { - t = currentServerTransaction(); - } - if (t == null || !t.isSkipCache()) { - id = spiQuery.getBeanDescriptor().cacheNaturalKeyIdLookup(spiQuery); - if (id != null) { - T bean = findIdCheckPersistenceContextAndCache(t, spiQuery, id); - if (bean != null) { - return bean; - } - } + if (transaction == null) { + transaction = currentServerTransaction(); } // a query that is expected to return either 0 or 1 beans - List list = findList(query, t); + List list = findList(query, transaction, true); return extractUnique(list); } @@ -1515,13 +1505,23 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } @Override - @SuppressWarnings("unchecked") public List findList(Query query, Transaction t) { + return findList(query, t, false); + } + + @SuppressWarnings("unchecked") + private List findList(Query query, Transaction t, boolean findOne) { SpiOrmQueryRequest request = createQueryRequest(Type.LIST, query, t); - Object result = request.getFromQueryCache(); - if (result != null) { - return (List) result; + if (!findOne) { + request.resetBeanCacheAutoMode(); + Object result = request.getFromQueryCache(); + if (result != null) { + return (List) result; + } + } + if ((t == null || !t.isSkipCache()) && request.getFromBeanCache()) { + return request.getBeanCacheHits(); } if (request.isUseDocStore()) { return docStore().findList(request); diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index e51516a24..63a1fd224 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -1,12 +1,14 @@ package io.ebeaninternal.server.core; import io.ebean.CacheMode; +import io.ebean.OrderBy; 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.BeanList; import io.ebean.common.CopyOnFirstWriteList; import io.ebean.event.BeanFindController; import io.ebean.event.BeanQueryAdapter; @@ -28,6 +30,9 @@ import io.ebeaninternal.server.deploy.DeployPropertyParserMap; import io.ebeaninternal.server.loadcontext.DLoadContext; import io.ebeaninternal.server.query.CQueryPlan; import io.ebeaninternal.server.query.CancelableQuery; +import io.ebeaninternal.api.BeanCacheResult; +import io.ebeaninternal.api.NaturalKeyQueryData; +import io.ebeaninternal.api.NaturalKeySet; import io.ebeaninternal.server.transaction.DefaultPersistenceContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,6 +78,8 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe private SpiQuerySecondary secondaryQueries; + private List cacheBeans; + /** * Create the InternalQueryRequest. */ @@ -495,8 +502,75 @@ public final class OrmQueryRequest extends BeanRequest implements BeanQueryRe beanDescriptor.putQueryPlan(queryPlanKey, queryPlan); } - public boolean isUseBeanCache() { - return query.isUseBeanCache(); + @Override + public void resetBeanCacheAutoMode() { + query.resetBeanCacheAutoMode(); + } + + public boolean isBeanCachePut() { + return query.isBeanCachePut(); + } + + /** + * Merge in prior L2 bean cache hits with the query result. + */ + public void mergeCacheHits(BeanCollection result) { + + if (cacheBeans != null && !cacheBeans.isEmpty()) { + for (T hit : cacheBeans) { + result.internalAdd(hit); + } + // resort in memory here after merging the cache hits with the DB hits + if (result instanceof BeanList) { + OrderBy orderBy = query.getOrderBy(); + if (orderBy != null) { + beanDescriptor.sort(((BeanList)result).getActualList(), orderBy.toStringFormat()); + } + } + } + } + + @Override + public List getBeanCacheHits() { + OrderBy orderBy = query.getOrderBy(); + if (orderBy != null) { + beanDescriptor.sort(cacheBeans, orderBy.toStringFormat()); + } + return cacheBeans; + } + + @Override + public boolean getFromBeanCache() { + + if (!query.isBeanCacheGet()) { + return false; + } + + // check if the query can use the bean cache + // 1. Find by Ids + // - hit beanCache with Ids + // - keep cache beans, ensure query modified to fetch misses + // - query and Load misses into bean cache + // - merge the 2 results and return + // + + if (!beanDescriptor.isNaturalKeyCaching()) { + return false; + } + + NaturalKeyQueryData data = query.naturalKey(); + if (data != null) { + NaturalKeySet naturalKeySet = data.buildKeys(); + if (naturalKeySet != null) { + // use the natural keys to lookup Ids to then hit the bean cache + BeanCacheResult cacheResult = beanDescriptor.naturalKeyLookup(persistenceContext, naturalKeySet.keys()); + // adjust the query (IN clause) based on the cache hits + this.cacheBeans = data.removeHits(cacheResult); + return data.allHits(); + } + } + + return false; } /** diff --git a/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java index 9d37205ba..281758b9d 100644 --- a/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -118,6 +118,24 @@ public interface SpiOrmQueryRequest extends DocQueryRequest { */ A getFromQueryCache(); + /** + * Maybe hit the bean cache returning true if everything was obtained from the + * cache (that there were no misses). + * + * Do this for findList() on many natural keys or many Ids. + */ + boolean getFromBeanCache(); + + /** + * Return the bean cache hits (when all hits / no misses). + */ + List getBeanCacheHits(); + + /** + * Reset Bean cache mode AUTO - require explicit setting for bean cache use with findList(). + */ + void resetBeanCacheAutoMode(); + /** * Return the Database platform like clause. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index df77705f8..450b60cf8 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -63,6 +63,7 @@ import io.ebeaninternal.server.persist.DmlUtil; import io.ebeaninternal.server.query.CQueryPlan; import io.ebeaninternal.server.query.CQueryPlanStats.Snapshot; import io.ebean.util.SplitName; +import io.ebeaninternal.api.BeanCacheResult; import io.ebeaninternal.server.querydefn.OrmQueryDetail; import io.ebeaninternal.server.rawsql.SpiRawSql; import io.ebeaninternal.server.text.json.ReadJson; @@ -93,6 +94,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -1189,6 +1191,13 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { } } + /** + * Return the natural key properties. + */ + public String[] getNaturalKey() { + return cacheHelp.getNaturalKey(); + } + /** * Return true if there is bean or query caching for this type. */ @@ -1204,6 +1213,13 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { return cacheHelp.isBeanCaching(); } + /** + * Return true if there is a natural key defined for this bean type. + */ + public boolean isNaturalKeyCaching() { + return cacheHelp.isNaturalKeyCaching(); + } + /** * Return true if there is query caching for this type of bean. */ @@ -1370,10 +1386,10 @@ public class BeanDescriptor implements MetaBeanInfo, BeanType { } /** - * Try to hit the cache using the natural key. + * Use natural key lookup to hit the bean cache. */ - public Object cacheNaturalKeyIdLookup(SpiQuery query) { - return cacheHelp.naturalKeyIdLookup(query); + public BeanCacheResult naturalKeyLookup(PersistenceContext context, Set keys) { + return cacheHelp.naturalKeyLookup(context, keys); } public void cacheNaturalKeyPut(Object id, Object newKey) { diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index 67b2a777a..b0dd83748 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -5,7 +5,7 @@ import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.PersistenceContext; import io.ebean.cache.ServerCache; -import io.ebeaninternal.api.SpiQuery; +import io.ebeaninternal.api.BeanCacheResult; import io.ebeaninternal.api.TransactionEventTable.TableIUD; import io.ebeaninternal.server.cache.CacheChangeSet; import io.ebeaninternal.server.cache.CachedBeanData; @@ -16,16 +16,18 @@ import io.ebeaninternal.server.cache.SpiCacheManager; import io.ebeaninternal.server.core.CacheOptions; import io.ebeaninternal.server.core.PersistRequest; import io.ebeaninternal.server.core.PersistRequestBean; -import io.ebeaninternal.server.querydefn.NaturalKeyBindParam; import io.ebeaninternal.server.transaction.DefaultPersistenceContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Helper for BeanDescriptor that manages the bean, query and collection caches. @@ -57,7 +59,7 @@ final class BeanDescriptorCacheHelp { private final String cacheName; private final BeanPropertyAssocOne[] propertiesOneImported; - private final String naturalKeyProperty; + private final String[] naturalKey; private final ServerCache beanCache; private final ServerCache naturalKeyCache; @@ -83,7 +85,7 @@ final class BeanDescriptorCacheHelp { this.cacheOptions = cacheOptions; this.cacheSharableBeans = cacheSharableBeans; this.propertiesOneImported = propertiesOneImported; - this.naturalKeyProperty = cacheOptions.getNaturalKey(); + this.naturalKey = cacheOptions.getNaturalKey(); if (!cacheOptions.isEnableQueryCache()) { this.queryCache = null; @@ -155,6 +157,13 @@ final class BeanDescriptorCacheHelp { return beanCache != null; } + /** + * Return true if there is natural key caching for this type of bean. + */ + boolean isNaturalKeyCaching() { + return naturalKeyCache != null; + } + /** * Return true if there is bean or query caching on this type. */ @@ -162,6 +171,13 @@ final class BeanDescriptorCacheHelp { return beanCache != null || queryCache != null; } + /** + * Return the natural key properties. + */ + String[] getNaturalKey() { + return naturalKey; + } + CacheOptions getCacheOptions() { return cacheOptions; } @@ -314,36 +330,49 @@ final class BeanDescriptorCacheHelp { } /** - * Find the bean using the natural key lookup if available. + * Use natural keys to hit the bean cache and return resulting hits. */ - Object naturalKeyIdLookup(SpiQuery query) { + BeanCacheResult naturalKeyLookup(PersistenceContext context, Set keys) { - if (!isNaturalKeyCaching(query.isUseBeanCache())) { - // no natural key caching for this query - return null; + if (context == null) { + context = new DefaultPersistenceContext(); } - // check if it is a find by unique id (using the natural key) - NaturalKeyBindParam keyBindParam = query.getNaturalKeyBindParam(); - if (keyBindParam == null || !isNaturalKey(keyBindParam.getName())) { - // query is not appropriate - return null; - } + // naturalKey -> Id map + Map naturalKeyMap = naturalKeyCache.getAll(keys); - // try to lookup the id using the natural key - Object id = naturalKeyCache.get(keyBindParam.getValue()); if (natLog.isTraceEnabled()) { - natLog.trace(" LOOKUP {}({}) - id:{}", cacheName, keyBindParam.getValue(), id); + natLog.trace(" LOOKUP Many {}({}) - hits:{}", cacheName, keys, naturalKeyMap); } - return id; - } - private boolean isNaturalKeyCaching(Boolean queryUseCache) { - return naturalKeyCache != null && (queryUseCache == null || queryUseCache); - } + BeanCacheResult result = new BeanCacheResult<>(); + if (naturalKeyMap.isEmpty()) { + return result; + } - private boolean isNaturalKey(String propName) { - return propName != null && propName.equals(cacheOptions.getNaturalKey()); + // create reverse id -> natural key map + Map reverseMap = new HashMap<>(); + for (Map.Entry entry : naturalKeyMap.entrySet()) { + reverseMap.put(entry.getValue(), entry.getKey()); + } + + Set ids = new HashSet<>(naturalKeyMap.values()); + Map beanDataMap = beanCache.getAll(ids); + if (beanLog.isTraceEnabled()) { + beanLog.trace(" GET MANY {}({}) - hits:{}", cacheName, ids, beanDataMap.keySet()); + } + // process the hits into beans etc + for (Map.Entry entry : beanDataMap.entrySet()) { + + Object id = entry.getKey(); + CachedBeanData cachedBeanData = (CachedBeanData) entry.getValue(); + + T bean = convertToBean(id, false, context, cachedBeanData); + Object naturalKey = reverseMap.get(id); + result.add(bean, naturalKey); + } + + return result; } /** @@ -414,8 +443,8 @@ final class BeanDescriptorCacheHelp { } getBeanCache().put(id, beanData); - if (naturalKeyProperty != null) { - Object naturalKey = beanData.getData(naturalKeyProperty); + if (naturalKey != null) { + Object naturalKey = calculateNaturalKey(beanData); if (naturalKey != null) { if (natLog.isDebugEnabled()) { natLog.debug(" PUT {}({}, {})", cacheName, naturalKey, id); @@ -425,6 +454,21 @@ final class BeanDescriptorCacheHelp { } } + private Object calculateNaturalKey(CachedBeanData beanData) { + if (naturalKey.length == 1) { + return beanData.getData(naturalKey[0]); + } + StringBuilder sb = new StringBuilder(); + for (String key : naturalKey) { + Object val = beanData.getData(key); + if (val == null) { + return null; + } + sb.append(val).append(";"); + } + return sb.toString(); + } + CachedBeanData beanCacheGetData(Object id) { return (CachedBeanData) getBeanCache().get(id); } @@ -450,6 +494,10 @@ final class BeanDescriptorCacheHelp { } return null; } + return convertToBean(id, readOnly, context, data); + } + + private T convertToBean(Object id, Boolean readOnly, PersistenceContext context, CachedBeanData data) { if (cacheSharableBeans && !Boolean.FALSE.equals(readOnly)) { Object bean = data.getSharableBean(); if (bean != null) { @@ -716,7 +764,7 @@ final class BeanDescriptorCacheHelp { } if (updateNaturalKey) { - Object oldKey = existingData.getData(naturalKeyProperty); + Object oldKey = calculateNaturalKey(existingData); if (oldKey != null) { if (natLog.isDebugEnabled()) { natLog.debug(".. update {} REMOVE({}) - old key for ({})", cacheName, oldKey, id); diff --git a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 055ef8ecb..396c3e06e 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -405,17 +405,17 @@ public class DeployBeanDescriptor { */ public void setCache(Cache cache) { - String naturalKey = null; - if (!cache.naturalKey().isEmpty()) { - // find the property and mark as natural key property - String propName = cache.naturalKey().trim(); - DeployBeanProperty beanProperty = getBeanProperty(propName); + String[] properties = cache.naturalKey(); + for (String property : properties) { + DeployBeanProperty beanProperty = getBeanProperty(property); if (beanProperty != null) { beanProperty.setNaturalKey(); - naturalKey = propName; } } - this.cacheOptions = new CacheOptions(cache, naturalKey); + if (properties.length == 0) { + properties = null; + } + this.cacheOptions = new CacheOptions(cache, properties); } /** diff --git a/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java b/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java index 4ad71dc94..bef8c338e 100644 --- a/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/AbstractExpression.java @@ -9,6 +9,7 @@ import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.el.ElPropertyDeploy; import io.ebeaninternal.server.el.ElPropertyValue; import io.ebean.util.SplitName; +import io.ebeaninternal.api.NaturalKeyQueryData; /** * Base class for simple expressions. @@ -21,6 +22,12 @@ public abstract class AbstractExpression implements SpiExpression { this.propName = propName; } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // by default can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java index 0ea2dc24f..6f35c9619 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExampleExpression.java @@ -4,6 +4,7 @@ import io.ebean.ExampleExpression; import io.ebean.LikeType; import io.ebean.bean.EntityBean; import io.ebean.event.BeanQueryRequest; +import io.ebean.util.SplitName; import io.ebeaninternal.api.ManyWhereJoins; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; @@ -11,7 +12,7 @@ import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.BeanProperty; import io.ebeaninternal.server.deploy.BeanPropertyAssocOne; -import io.ebean.util.SplitName; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.util.ArrayList; @@ -87,6 +88,12 @@ public class DefaultExampleExpression implements SpiExpression, ExampleExpressio } } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java index cb31c5008..8e2a4f895 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionList.java @@ -27,6 +27,7 @@ import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.api.SpiJunction; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.sql.Timestamp; @@ -123,6 +124,12 @@ public class DefaultExpressionList implements SpiExpressionList { return new JunctionExpression<>(Junction.Type.FILTER, this); } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { simplifyEntries(); diff --git a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java index 6f800cfbf..c6eeca3c1 100644 --- a/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java +++ b/src/main/java/io/ebeaninternal/server/expression/DefaultExpressionRequest.java @@ -161,7 +161,7 @@ public class DefaultExpressionRequest implements SpiExpressionRequest { } @Override - public void appendInExpression(boolean not, Object[] bindValues) { + public void appendInExpression(boolean not, List bindValues) { append(binder.getInExpression(not, bindValues)); } } diff --git a/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java b/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java index c7e9c1159..37fa4d565 100644 --- a/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/ExistsQueryExpression.java @@ -9,6 +9,7 @@ import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.query.CQuery; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.util.List; @@ -35,6 +36,12 @@ class ExistsQueryExpression implements SpiExpression, UnsupportedDocStoreExpress this.subQuery = null; } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing diff --git a/src/main/java/io/ebeaninternal/server/expression/InExpression.java b/src/main/java/io/ebeaninternal/server/expression/InExpression.java index 58c479058..daf2a3f57 100644 --- a/src/main/java/io/ebeaninternal/server/expression/InExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/InExpression.java @@ -6,6 +6,7 @@ import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.server.el.ElPropertyValue; import io.ebeaninternal.server.persist.MultiValueWrapper; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.util.ArrayList; @@ -20,7 +21,7 @@ class InExpression extends AbstractExpression { private final Collection sourceValues; - private Object[] bindValues; + private List bindValues; private boolean multiValueSupported; @@ -36,25 +37,31 @@ class InExpression extends AbstractExpression { this.not = not; } - private Object[] values() { + private List values() { List vals = new ArrayList<>(sourceValues.size()); for (Object sourceValue : sourceValues) { NamedParamHelp.valueAdd(vals, sourceValue); } - return vals.toArray(); + return vals; + } + + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache for NOT IN + return !not && data.matchIn(propName, bindValues); } @Override public void prepareExpression(BeanQueryRequest request) { bindValues = values(); - if (bindValues.length > 0) { - multiValueSupported = request.isMultiValueSupported((bindValues[0]).getClass()); + if (bindValues.size() > 0) { + multiValueSupported = request.isMultiValueSupported((bindValues.get(0)).getClass()); } } @Override public void writeDocQuery(DocQueryContext context) throws IOException { - context.writeIn(propName, values(), not); + context.writeIn(propName, values().toArray(), not); } @Override @@ -66,10 +73,10 @@ class InExpression extends AbstractExpression { } if (prop == null) { - if (bindValues.length > 0) { + if (bindValues.size() > 0) { // if we have no property, we wrap them in a multi value wrapper. // later the binder will decide, which bind strategy to use. - request.addBindValue(new MultiValueWrapper(Arrays.asList(bindValues))); + request.addBindValue(new MultiValueWrapper(bindValues)); } } else { List idList = new ArrayList<>(); @@ -89,7 +96,7 @@ class InExpression extends AbstractExpression { @Override public void addSql(SpiExpressionRequest request) { - if (bindValues.length == 0) { + if (bindValues.isEmpty()) { String expr = not ? "1=1" : "1=0"; request.append(expr); return; @@ -102,7 +109,7 @@ class InExpression extends AbstractExpression { if (prop != null) { request.append(prop.getAssocIdInExpr(propName)); - String inClause = prop.getAssocIdInValueExpr(not, bindValues.length); + String inClause = prop.getAssocIdInValueExpr(not, bindValues.size()); request.append(inClause); } else { @@ -125,7 +132,7 @@ class InExpression extends AbstractExpression { builder.append(" ?"); if (!multiValueSupported) { // query plan specific to the number of parameters in the IN clause - builder.append(bindValues.length); + builder.append(bindValues.size()); } builder.append("]"); } @@ -142,11 +149,11 @@ class InExpression extends AbstractExpression { @Override public boolean isSameByBind(SpiExpression other) { InExpression that = (InExpression) other; - if (this.bindValues.length != that.bindValues.length) { + if (this.bindValues.size() != that.bindValues.size()) { return false; } - for (int i = 0; i < bindValues.length; i++) { - if (!bindValues[i].equals(that.bindValues[i])) { + for (int i = 0; i < bindValues.size(); i++) { + if (!bindValues.get(i).equals(that.bindValues.get(i))) { return false; } } diff --git a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java index 44ee27cd5..180a154f1 100644 --- a/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/JunctionExpression.java @@ -25,6 +25,7 @@ import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.api.SpiJunction; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.sql.Timestamp; @@ -58,6 +59,12 @@ class JunctionExpression implements SpiJunction, SpiExpression, Expression this.exprList = exprList; } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + /** * Simplify nested expressions where possible. *

diff --git a/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java b/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java index 2b02c4e21..670abb0ce 100644 --- a/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/LogicExpression.java @@ -8,6 +8,7 @@ import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; @@ -56,6 +57,12 @@ abstract class LogicExpression implements SpiExpression { this.expTwo = (SpiExpression) expTwo; } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing diff --git a/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java b/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java index 2a5a942ce..61e0d82c8 100644 --- a/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/NestedPathWrapperExpression.java @@ -6,6 +6,7 @@ import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; @@ -23,6 +24,12 @@ class NestedPathWrapperExpression implements SpiExpression { this.delegate = delegate; } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing @@ -59,7 +66,7 @@ class NestedPathWrapperExpression implements SpiExpression { @Override public void queryPlanHash(StringBuilder builder) { builder.append("NestedPath["); - if (nestedPath != null){ + if (nestedPath != null) { builder.append("path:").append(nestedPath).append(" "); } delegate.queryPlanHash(builder); diff --git a/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java b/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java index 21507b4d3..f27ab9f97 100644 --- a/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/NonPrepareExpression.java @@ -2,12 +2,19 @@ package io.ebeaninternal.server.expression; import io.ebean.event.BeanQueryRequest; import io.ebeaninternal.api.SpiExpression; +import io.ebeaninternal.api.NaturalKeyQueryData; /** * Base abstract expression that does nothing for prepareExpression(). */ abstract class NonPrepareExpression implements SpiExpression { + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing diff --git a/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java b/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java index adc609fad..28ff54916 100644 --- a/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/NoopExpression.java @@ -6,6 +6,7 @@ import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; @@ -16,6 +17,12 @@ class NoopExpression implements SpiExpression { protected static final NoopExpression INSTANCE = new NoopExpression(); + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing diff --git a/src/main/java/io/ebeaninternal/server/expression/NotExpression.java b/src/main/java/io/ebeaninternal/server/expression/NotExpression.java index 8ff526ee6..264d0d3db 100644 --- a/src/main/java/io/ebeaninternal/server/expression/NotExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/NotExpression.java @@ -7,6 +7,7 @@ import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.api.SpiExpressionValidation; import io.ebeaninternal.server.deploy.BeanDescriptor; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; @@ -21,6 +22,12 @@ final class NotExpression implements SpiExpression { this.exp = (SpiExpression) exp; } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache + return false; + } + @Override public void simplify() { // do nothing diff --git a/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java b/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java index ee88e8a89..e653873eb 100644 --- a/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/SimpleExpression.java @@ -5,6 +5,7 @@ import io.ebean.plugin.ExpressionPath; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionRequest; import io.ebeaninternal.server.el.ElPropertyValue; +import io.ebeaninternal.api.NaturalKeyQueryData; import java.io.IOException; import java.util.Arrays; @@ -26,6 +27,15 @@ public class SimpleExpression extends AbstractValueExpression { return null; } + @Override + public boolean naturalKey(NaturalKeyQueryData data) { + // can't use naturalKey cache for NOT IN + if (type != Op.EQ) { + return false; + } + return data.matchEq(propName, bindValue); + } + @Override public void writeDocQuery(DocQueryContext context) throws IOException { if (type == Op.BETWEEN) { diff --git a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java index 0922af599..21e6c35d3 100644 --- a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java +++ b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadBaseContext.java @@ -39,9 +39,7 @@ public abstract class DLoadBaseContext { this.desc = desc; this.queryProps = queryProps; this.fullPath = parent.getFullPath(path); - - this.hitCache = !parent.isExcludeBeanCache() && desc.isBeanCaching(); - + this.hitCache = parent.isBeanCacheGet() && desc.isBeanCaching(); this.objectGraphNode = parent.getObjectGraphNode(path); this.queryFetch = queryProps != null && queryProps.isQueryFetch(); diff --git a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java index a04848703..5300979ac 100644 --- a/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java +++ b/src/main/java/io/ebeaninternal/server/loadcontext/DLoadContext.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.loadcontext; +import io.ebean.CacheMode; import io.ebean.bean.BeanCollection; import io.ebean.bean.CallStack; import io.ebean.bean.EntityBeanIntercept; @@ -41,7 +42,7 @@ public class DLoadContext implements LoadContext { private final boolean asDraft; private final Timestamp asOf; private final Boolean readOnly; - private final boolean excludeBeanCache; + private final CacheMode useBeanCache; private final int defaultBatchSize; private final boolean disableLazyLoading; private final boolean disableReadAudit; @@ -74,7 +75,7 @@ public class DLoadContext implements LoadContext { this.persistenceContext = persistenceContext; this.origin = initOrigin(); this.defaultBatchSize = 100; - this.excludeBeanCache = false; + this.useBeanCache = CacheMode.OFF; this.asDraft = false; this.asOf = null; this.readOnly = false; @@ -107,7 +108,7 @@ public class DLoadContext implements LoadContext { this.readOnly = query.isReadOnly(); this.disableReadAudit = query.isDisableReadAudit(); this.disableLazyLoading = query.isDisableLazyLoading(); - this.excludeBeanCache = query.isExcludeBeanCache(); + this.useBeanCache = query.getUseBeanCache(); this.useProfiling = query.getProfilingListener() != null; ObjectGraphNode parentNode = query.getParentNode(); @@ -158,8 +159,8 @@ public class DLoadContext implements LoadContext { registerSecondaryNode(many, props); } - protected boolean isExcludeBeanCache() { - return excludeBeanCache; + protected boolean isBeanCacheGet() { + return useBeanCache.isGet(); } /** diff --git a/src/main/java/io/ebeaninternal/server/persist/Binder.java b/src/main/java/io/ebeaninternal/server/persist/Binder.java index 34a3f8f2c..9c673a5f3 100644 --- a/src/main/java/io/ebeaninternal/server/persist/Binder.java +++ b/src/main/java/io/ebeaninternal/server/persist/Binder.java @@ -229,9 +229,9 @@ public class Binder { /** * Return the SQL in clause taking into account Multi-value support. */ - public String getInExpression(boolean not, Object[] bindValues) { - ScalarType type = getScalarType(bindValues[0].getClass()); - return multiValueBind.getInExpression(not, type, bindValues.length); + public String getInExpression(boolean not, List bindValues) { + ScalarType type = getScalarType(bindValues.get(0).getClass()); + return multiValueBind.getInExpression(not, type, bindValues.size()); } /** diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index a327f3882..5b3758eb9 100644 --- a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -151,7 +151,7 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { SpiQuery query = request.getQuery(); - if (query.isLoadBeanCache()) { + if (query.isBeanCachePut()) { // load the individual beans into the bean cache BeanDescriptor descriptor = request.getBeanDescriptor(); Collection c = result.getActualDetails(); @@ -160,6 +160,8 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { } } + request.mergeCacheHits(result); + if (!result.isEmpty() && query.getUseQueryCache().isPut()) { // load the query result into the query cache result.setReadOnly(true); @@ -189,7 +191,7 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine { result = queryEngine.find(request); } - if (result != null && request.isUseBeanCache()) { + if (result != null && request.isBeanCachePut()) { request.getBeanDescriptor().cacheBeanPut((EntityBean) result); } diff --git a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 2f0b14715..ec52298f2 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -29,6 +29,7 @@ import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.CQueryPlanKey; import io.ebeaninternal.api.HashQuery; import io.ebeaninternal.api.ManyWhereJoins; +import io.ebeaninternal.api.NaturalKeyQueryData; import io.ebeaninternal.api.SpiExpression; import io.ebeaninternal.api.SpiExpressionList; import io.ebeaninternal.api.SpiExpressionValidation; @@ -193,9 +194,7 @@ public class DefaultOrmQuery implements SpiQuery { private boolean usageProfiling = true; - private boolean loadBeanCache; - - private boolean excludeBeanCache; + private CacheMode useBeanCache = CacheMode.AUTO; private CacheMode useQueryCache = CacheMode.OFF; @@ -375,6 +374,7 @@ public class DefaultOrmQuery implements SpiQuery { @Override public DefaultOrmQuery asDraft() { this.temporalMode = TemporalMode.DRAFT; + this.useBeanCache = CacheMode.OFF; return this; } @@ -641,6 +641,28 @@ public class DefaultOrmQuery implements SpiQuery { return !distinct && !singleAttribute; } + @Override + public NaturalKeyQueryData naturalKey() { + + if (whereExpressions == null) { + return null; + } + String[] naturalKey = beanDescriptor.getNaturalKey(); + if (naturalKey == null || naturalKey.length == 0) { + return null; + } + + NaturalKeyQueryData data = new NaturalKeyQueryData<>(naturalKey); + for (SpiExpression expression : whereExpressions.getUnderlyingList()) { + // must be eq or in + if (!expression.naturalKey(data)) { + return null; + } + } + + return data; + } + @Override public NaturalKeyBindParam getNaturalKeyBindParam() { NaturalKeyBindParam namedBind = null; @@ -685,15 +707,13 @@ public class DefaultOrmQuery implements SpiQuery { copy.m2mIncludeJoin = m2mIncludeJoin; copy.profilingListener = profilingListener; -// copy.query = query; copy.rootTableAlias = rootTableAlias; copy.distinct = distinct; copy.sqlDistinct = sqlDistinct; copy.timeout = timeout; copy.mapKey = mapKey; copy.id = id; - copy.loadBeanCache = loadBeanCache; - copy.excludeBeanCache = excludeBeanCache; + copy.useBeanCache = useBeanCache; copy.useQueryCache = useQueryCache; copy.readOnly = readOnly; if (detail != null) { @@ -833,7 +853,7 @@ public class DefaultOrmQuery implements SpiQuery { @Override public DefaultOrmQuery setForUpdate(boolean forUpdate) { this.forUpdate = (forUpdate) ? ForUpdate.BASE : null; - this.excludeBeanCache = true; + this.useBeanCache = CacheMode.OFF; return this; } @@ -854,7 +874,7 @@ public class DefaultOrmQuery implements SpiQuery { private DefaultOrmQuery setForUpdateWithMode(ForUpdate mode) { this.forUpdate = mode; - this.excludeBeanCache = true; + this.useBeanCache = CacheMode.OFF; return this; } @@ -1083,29 +1103,40 @@ public class DefaultOrmQuery implements SpiQuery { } @Override - public boolean isExcludeBeanCache() { - // not using L2 cache for asDraft() query - return excludeBeanCache || isAsDraft(); + public boolean isBeanCachePut() { + return beanDescriptor.isBeanCaching() && useBeanCache.isPut(); } @Override - public boolean isUseBeanCache() { - return !isExcludeBeanCache() && beanDescriptor.isBeanCaching(); + public boolean isBeanCacheGet() { + return beanDescriptor.isBeanCaching() && useBeanCache.isGet(); } @Override - public CacheMode getUseQueryCache() { - // not using L2 cache for asDraft() query - if (isAsDraft()) { - return CacheMode.OFF; - } else { - return useQueryCache; + public boolean isBeanCacheReload() { + return CacheMode.RECACHE == useBeanCache; + } + + @Override + public void resetBeanCacheAutoMode() { + if (useBeanCache == CacheMode.AUTO) { + useBeanCache = CacheMode.OFF; } } + @Override + public CacheMode getUseBeanCache() { + return useBeanCache; + } + + @Override + public CacheMode getUseQueryCache() { + return useQueryCache; + } + @Override public DefaultOrmQuery setUseCache(boolean useCache) { - this.excludeBeanCache = !useCache; + this.useBeanCache = (useCache) ? CacheMode.ON: CacheMode.OFF; return this; } @@ -1115,15 +1146,9 @@ public class DefaultOrmQuery implements SpiQuery { return this; } - @Override - public boolean isLoadBeanCache() { - // not using L2 cache for asDraft() query - return !isAsDraft() && loadBeanCache; - } - @Override public DefaultOrmQuery setLoadBeanCache(boolean loadBeanCache) { - this.loadBeanCache = loadBeanCache; + this.useBeanCache = CacheMode.RECACHE; return this; } diff --git a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java index e60bb4e73..47ee67204 100644 --- a/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java +++ b/src/test/java/io/ebeaninternal/server/expression/TDSpiExpressionRequest.java @@ -85,7 +85,7 @@ public class TDSpiExpressionRequest implements SpiExpressionRequest { } @Override - public void appendInExpression(boolean not, Object[] bindValues) { + public void appendInExpression(boolean not, List bindValues) { } } diff --git a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java index a92ccf92a..28047d00b 100644 --- a/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java +++ b/src/test/java/io/ebeaninternal/server/querydefn/DefaultOrmQueryTest.java @@ -2,6 +2,7 @@ package io.ebeaninternal.server.querydefn; import io.ebean.BaseTestCase; +import io.ebean.CacheMode; import io.ebean.Ebean; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.core.OrmQueryRequest; @@ -19,7 +20,7 @@ public class DefaultOrmQueryTest extends BaseTestCase { DefaultOrmQuery q1 = (DefaultOrmQuery) Ebean.find(Customer.class) .setForUpdate(true).where().eq("id", 42).query(); - assertThat(q1.isExcludeBeanCache()).isTrue(); + assertThat(q1.getUseBeanCache()).isSameAs(CacheMode.OFF); } @Test diff --git a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java index e2d98199c..d440cc884 100644 --- a/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java +++ b/src/test/java/org/tests/batchload/TestBatchLazyWithCacheHits.java @@ -3,7 +3,6 @@ package org.tests.batchload; import io.ebean.BaseTestCase; import io.ebean.Ebean; import io.ebean.cache.ServerCache; -import io.ebean.cache.ServerCacheManager; import io.ebean.cache.ServerCacheStatistics; import org.tests.model.basic.UUOne; import org.ebeantest.LoggedSqlCollector; @@ -18,6 +17,8 @@ import static org.junit.Assert.assertNotNull; public class TestBatchLazyWithCacheHits extends BaseTestCase { + private ServerCache beanCache = server().getServerCacheManager().getBeanCache(UUOne.class); + private UUOne insert(String name) { UUOne one = new UUOne(); one.setName("testBLWCH" + name); @@ -28,24 +29,17 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase { @Test public void testOnCacheHit() { - ArrayList inserted = new ArrayList<>(); - String[] names = "A,B,C,D,E,F,G,H,I,J".split(","); - for (String name : names) { - inserted.add(insert(name)); - } + ArrayList inserted = insertData(); - ServerCacheManager serverCacheManager = Ebean.getDefaultServer().getServerCacheManager(); - ServerCache beanCache = serverCacheManager.getBeanCache(UUOne.class); - beanCache.clear(); - beanCache.getStatistics(true); // Reset statistics - otherwise other tests will interfere + clearCacheAndStatistics(); UUOne b = Ebean.find(UUOne.class, inserted.get(1).getId()); assertNotNull(b); UUOne b2 = Ebean.find(UUOne.class, inserted.get(1).getId()); assertNotNull(b2); - ServerCacheStatistics statistics = beanCache.getStatistics(true); - assertEquals(statistics.getHitCount(), 1); + + assertBeanCacheHits(1); UUOne c = Ebean.find(UUOne.class) .where().idEq(inserted.get(2).getId()) @@ -55,9 +49,9 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase { UUOne c2 = Ebean.find(UUOne.class) .where().idEq(inserted.get(2).getId()) .findOne(); + assertNotNull(c2); - statistics = beanCache.getStatistics(true); - assertEquals(statistics.getHitCount(), 1); + assertBeanCacheHits(1); LoggedSqlCollector.start(); @@ -76,13 +70,33 @@ public class TestBatchLazyWithCacheHits extends BaseTestCase { List sql = LoggedSqlCollector.stop(); System.out.println("sql:" + sql); - // batch lazy loading into cache assertThat(sql).hasSize(2); assertThat(sql.get(0)).contains("from uuone t0 where t0.name like "); platformAssertIn(sql.get(1), "from uuone t0 where t0.id"); - statistics = beanCache.getStatistics(true); - assertThat(statistics.getSize()).isGreaterThan(3); + // not lazy loading into bean cache + int size = beanCache.getStatistics(true).getSize(); + assertThat(size).isEqualTo(2); + } + + private void assertBeanCacheHits(int hits) { + ServerCacheStatistics statistics = beanCache.getStatistics(true); + assertEquals(statistics.getHitCount(), hits); + } + + private void clearCacheAndStatistics() { + + beanCache.clear(); + beanCache.getStatistics(true); + } + + private ArrayList insertData() { + ArrayList inserted = new ArrayList<>(); + String[] names = "A,B,C,D,E,F,G,H,I,J".split(","); + for (String name : names) { + inserted.add(insert(name)); + } + return inserted; } } diff --git a/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean.java b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean.java new file mode 100644 index 000000000..592818113 --- /dev/null +++ b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean.java @@ -0,0 +1,62 @@ +package org.tests.model.basic.cache; + +import io.ebean.annotation.Cache; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * Cached bean for testing caching implementation. + */ +@Cache(naturalKey = {"store","sku"}) +@Entity +@Table(name = "o_cached_natkey") +public class OCachedNatKeyBean { + + @Id + Long id; + + String store; + + String sku; + + String description; + + OCachedNatKeyBean(String store, String sku) { + this.store = store; + this.sku = sku; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStore() { + return store; + } + + public void setStore(String store) { + this.store = store; + } + + public String getSku() { + return sku; + } + + public void setSku(String sku) { + this.sku = sku; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean3.java b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean3.java new file mode 100644 index 000000000..92cbde2ec --- /dev/null +++ b/src/test/java/org/tests/model/basic/cache/OCachedNatKeyBean3.java @@ -0,0 +1,65 @@ +package org.tests.model.basic.cache; + +import io.ebean.annotation.Cache; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * Cached bean for compound natural key. + */ +@Cache(naturalKey = {"store","code","sku"}) +@Entity +@Table(name = "o_cached_natkey3") +public class OCachedNatKeyBean3 { + + @Id + Long id; + + String store; + + int code; + + String sku; + + String description; + + OCachedNatKeyBean3(String store, int code, String sku) { + this.store = store; + this.code = code; + this.sku = sku; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStore() { + return store; + } + + public void setStore(String store) { + this.store = store; + } + + public String getSku() { + return sku; + } + + public void setSku(String sku) { + this.sku = sku; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java new file mode 100644 index 000000000..45dab5f70 --- /dev/null +++ b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java @@ -0,0 +1,238 @@ +package org.tests.model.basic.cache; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.cache.ServerCache; +import io.ebean.cache.ServerCacheManager; +import io.ebean.cache.ServerCacheStatistics; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestCacheViaComplexNaturalKey extends BaseTestCase { + + private ServerCache beanCache = cacheManager().getBeanCache(OCachedNatKeyBean.class); + private ServerCache natKeyCache = cacheManager().getNaturalKeyCache(OCachedNatKeyBean.class); + + private static boolean loadOnce; + + private ServerCacheManager cacheManager() { + return server().getServerCacheManager(); + } + + private static synchronized void insertSome() { + if (!loadOnce) { + Ebean.find(OCachedNatKeyBean.class).delete(); + + List stores = new ArrayList<>(Arrays.asList("abc", "def")); + for (String store : stores) { + List skus = new ArrayList<>(Arrays.asList("1", "2", "3", "4")); + for (String sku : skus) { + saveBean(store, sku); + } + } + loadOnce = true; + } + } + + private static void saveBean(String abc, String sku) { + Ebean.save(new OCachedNatKeyBean(abc, sku)); + } + + private void clearCacheAndStatistics() { + Ebean.getServerCacheManager().clearAll(); + clearStatistics(); + } + + private void clearStatistics() { + beanCache.getStatistics(true); + natKeyCache.getStatistics(true); + } + + private void assertNaturalKeyHitMiss(int expectedHit, int expectedMiss) { + ServerCacheStatistics stats = natKeyCache.getStatistics(true); + assertHitMiss(expectedHit, expectedMiss, stats); + } + + private void assertBeanCacheHitMiss(int expectedHit, int expectedMiss) { + ServerCacheStatistics stats = beanCache.getStatistics(true); + assertHitMiss(expectedHit, expectedMiss, stats); + } + + private void assertHitMiss(int expectedHit, int expectedMiss, ServerCacheStatistics stats) { + assertThat(stats.getHitCount()).isEqualTo(expectedHit); + assertThat(stats.getMissCount()).isEqualTo(expectedMiss); + } + + private void setup() { + insertSome(); + clearCacheAndStatistics(); + } + + private void loadSomeIntoCache() { + + Ebean.find(OCachedNatKeyBean.class) + .setLoadBeanCache(true) + .where().le("sku", "2") + .findList(); + + clearStatistics(); + } + + @Test + public void findList_inClause_someHits() { + + setup(); + loadSomeIntoCache(); + + String storeId = "abc"; + List skus = new ArrayList<>(Arrays.asList("1", "2", "3")); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", storeId) + .in("sku", skus) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + if (isH2()) { + // in clause with only 1 bind param - (sku=3 ... we got hits on sku 1 and 2) + assertThat(sql.get(0)).contains("from o_cached_natkey t0 where t0.store = ? and t0.sku in (? ) order by t0.sku desc; --bind(abc,Array[1]={3})"); + } + + assertThat(list).hasSize(3); + + assertNaturalKeyHitMiss(2, 1); + assertBeanCacheHitMiss(2, 0); + } + + @Test + public void findList_inClause_allHits() { + + setup(); + loadSomeIntoCache(); + + String storeId = "abc"; + List skus = new ArrayList<>(Arrays.asList("1", "2")); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", storeId) + .in("sku", skus) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSqlCollector.stop(); + + // no SQL - all beans from cache + assertThat(sql).isEmpty(); + + assertThat(list).hasSize(2); + assertNaturalKeyHitMiss(2, 0); + assertBeanCacheHitMiss(2, 0); + } + + @Test + public void findList_inClause_noHits() { + + setup(); + loadSomeIntoCache(); + + String storeId = "abc"; + List skus = new ArrayList<>(Arrays.asList("3", "4")); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", storeId) + .in("sku", skus) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + if (isH2()) { + // in clause with 2 bind params as we got not hits on the cache + assertThat(sql.get(0)).contains("from o_cached_natkey t0 where t0.store = ? and t0.sku in (?, ? ) order by t0.sku desc; --bind(abc,Array[2]={3,4})"); + } + + assertThat(list).hasSize(2); + assertNaturalKeyHitMiss(0, 2); + assertBeanCacheHitMiss(0, 0); + } + + @Test + public void hit() { + + setup(); + loadSomeIntoCache(); + + // simple case - no IN clause + OCachedNatKeyBean hit = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", "abc") + .eq("sku", "2") + .findOne(); + + assertThat(hit).isNotNull(); + + assertNaturalKeyHitMiss(1, 0); + assertBeanCacheHitMiss(1, 0); + } + + @Test + public void miss() { + + setup(); + + OCachedNatKeyBean miss = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", "def") + .eq("sku", "4") + .findOne(); + + assertThat(miss).isNotNull(); + + // miss on natural key + assertNaturalKeyHitMiss(0, 1); + + // no activity against bean cache + assertBeanCacheHitMiss(0, 0); + } + + + @Test + public void explicitUseCacheFalse_expect_noUseOfCache() { + + setup(); + + OCachedNatKeyBean miss = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", "def") + .eq("sku", "4") + .setUseCache(false) // explicitly not use cache + .findOne(); + + assertThat(miss).isNotNull(); + + // no activity against either cache + assertNaturalKeyHitMiss(0, 0); + assertBeanCacheHitMiss(0, 0); + } + +} diff --git a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java new file mode 100644 index 000000000..26919f861 --- /dev/null +++ b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey3.java @@ -0,0 +1,250 @@ +package org.tests.model.basic.cache; + +import io.ebean.BaseTestCase; +import io.ebean.Ebean; +import io.ebean.cache.ServerCache; +import io.ebean.cache.ServerCacheManager; +import io.ebean.cache.ServerCacheStatistics; +import org.ebeantest.LoggedSqlCollector; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestCacheViaComplexNaturalKey3 extends BaseTestCase { + + private ServerCache beanCache = cacheManager().getBeanCache(OCachedNatKeyBean3.class); + private ServerCache natKeyCache = cacheManager().getNaturalKeyCache(OCachedNatKeyBean3.class); + + private static boolean loadOnce; + + private ServerCacheManager cacheManager() { + return server().getServerCacheManager(); + } + + private static synchronized void insertSome() { + if (!loadOnce) { + Ebean.find(OCachedNatKeyBean3.class).delete(); + + List stores = new ArrayList<>(Arrays.asList("abc", "def")); + for (String store : stores) { + List skus = new ArrayList<>(Arrays.asList("1", "2", "3")); + for (String sku : skus) { + int[] codes = {1000,1001,1002,1003,1004}; + for (int code : codes) { + saveBean(store, code, sku); + } + } + } + loadOnce = true; + } + } + + private static void saveBean(String store, int code, String sku) { + Ebean.save(new OCachedNatKeyBean3(store, code, sku)); + } + + private void clearCacheAndStatistics() { + Ebean.getServerCacheManager().clearAll(); + clearStatistics(); + } + + private void clearStatistics() { + beanCache.getStatistics(true); + natKeyCache.getStatistics(true); + } + + private void assertNaturalKeyHitMiss(int expectedHit, int expectedMiss) { + ServerCacheStatistics stats = natKeyCache.getStatistics(true); + assertHitMiss(expectedHit, expectedMiss, stats); + } + + private void assertBeanCacheHitMiss(int expectedHit, int expectedMiss) { + ServerCacheStatistics stats = beanCache.getStatistics(true); + assertHitMiss(expectedHit, expectedMiss, stats); + } + + private void assertHitMiss(int expectedHit, int expectedMiss, ServerCacheStatistics stats) { + assertThat(stats.getHitCount()).isEqualTo(expectedHit); + assertThat(stats.getMissCount()).isEqualTo(expectedMiss); + } + + private void setup() { + insertSome(); + clearCacheAndStatistics(); + } + + private void loadSomeIntoCache() { + + Ebean.find(OCachedNatKeyBean3.class) + .setLoadBeanCache(true) + .where() + .ge("sku", "2") + .eq("store", "def") + .ge("code", 1001) + .findList(); + + clearStatistics(); + } + + @Test + public void findList_inClause_someHits() { + + setup(); + loadSomeIntoCache(); + + + List codes = new ArrayList<>(Arrays.asList(1001, 1000, 1002, 1003)); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "def") + .in("code", codes) + .eq("sku", "2") + .setUseCache(true) + .order().desc("sku") + .order().asc("code") + .findList(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + if (isH2()) { + // in clause with only 1 bind param - (sku=3 ... we got hits on sku 1 and 2) + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and t0.code in (? ) and t0.sku = ? order by t0.sku desc, t0.code; --bind(def,Array[1]={1000},2)"); + } + + assertThat(list).hasSize(4); + + assertNaturalKeyHitMiss(3, 1); + assertBeanCacheHitMiss(3, 0); + } + + @Test + public void findList_inClause_allHits() { + + setup(); + loadSomeIntoCache(); + + List skus = new ArrayList<>(Arrays.asList("2", "3")); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "def") + .in("sku", skus) + .eq("code", 1001) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSqlCollector.stop(); + + // no SQL - all beans from cache + assertThat(sql).isEmpty(); + + assertThat(list).hasSize(2); + assertNaturalKeyHitMiss(2, 0); + assertBeanCacheHitMiss(2, 0); + } + + @Test + public void findList_inClause_noHits() { + + setup(); + loadSomeIntoCache(); + + String storeId = "abc"; + List skus = new ArrayList<>(Arrays.asList("3", "2", "4")); + + LoggedSqlCollector.start(); + + List list = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", storeId) + .in("sku", skus) + .eq("code", 1001) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + List sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + if (isH2()) { + // in clause with 2 bind params as we got not hits on the cache + assertThat(sql.get(0)).contains("from o_cached_natkey3 t0 where t0.store = ? and t0.sku in (?, ?, ? ) and t0.code = ? order by t0.sku desc; --bind(abc,Array[3]={3,2,4},1001)"); + } + + assertThat(list).hasSize(2); + assertNaturalKeyHitMiss(0, 3); + assertBeanCacheHitMiss(0, 0); + } + + + @Test + public void hit() { + + setup(); + loadSomeIntoCache(); + + // simple case - no IN clause + OCachedNatKeyBean3 hit = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "def") + .eq("sku", "2") + .eq("code", 1002) + .findOne(); + + assertThat(hit).isNotNull(); + + assertNaturalKeyHitMiss(1, 0); + assertBeanCacheHitMiss(1, 0); + } + + @Test + public void miss() { + + setup(); + + OCachedNatKeyBean3 miss = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "abc") + .eq("sku", "2") + .eq("code", 1000) + .findOne(); + + assertThat(miss).isNotNull(); + + // miss on natural key + assertNaturalKeyHitMiss(0, 1); + + // no activity against bean cache + assertBeanCacheHitMiss(0, 0); + } + + @Test + public void explicitUseCacheFalse_expect_noUseOfCache() { + + setup(); + + OCachedNatKeyBean3 miss = Ebean.find(OCachedNatKeyBean3.class) + .where() + .eq("store", "abc") + .eq("sku", "2") + .eq("code", 1000) + .setUseCache(false) // explicitly not use cache + .findOne(); + + assertThat(miss).isNotNull(); + + // no activity against either cache + assertNaturalKeyHitMiss(0, 0); + assertBeanCacheHitMiss(0, 0); + } + +}