From 65fa1c056b67d3b83317a7244627c4881acd7915 Mon Sep 17 00:00:00 2001 From: rob bygrave Date: Fri, 26 Apr 2019 12:01:16 +1200 Subject: [PATCH] #1681 - Support hitting L2 bean cache via ... findList() with `idsIn()` expression --- .../io/ebeaninternal/api/CacheIdLookup.java | 52 +++++++++ .../api/NaturalKeyQueryData.java | 32 +++--- .../java/io/ebeaninternal/api/SpiQuery.java | 5 + .../server/core/DefaultServer.java | 30 +++-- .../server/core/OrmQueryRequest.java | 12 +- .../server/core/SpiOrmQueryRequest.java | 5 + .../server/deploy/BeanDescriptor.java | 4 + .../deploy/BeanDescriptorCacheHelp.java | 45 ++++---- .../server/expression/IdInExpression.java | 22 +++- .../server/expression/InExpression.java | 20 ++-- .../server/expression/InPairsExpression.java | 1 - .../server/query/CQueryBindCapture.java | 2 +- .../server/query/DQueryPlanOutput.java | 18 +-- .../server/query/QueryPlanLogger.java | 2 +- .../server/querydefn/DefaultOrmQuery.java | 20 +++- .../java/org/tests/cache/TestBeanCache.java | 107 ++++++++++++++++++ .../cache/personinfo/PersonCacheTests.java | 6 +- 17 files changed, 304 insertions(+), 79 deletions(-) create mode 100644 src/main/java/io/ebeaninternal/api/CacheIdLookup.java diff --git a/src/main/java/io/ebeaninternal/api/CacheIdLookup.java b/src/main/java/io/ebeaninternal/api/CacheIdLookup.java new file mode 100644 index 000000000..0ae791747 --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/CacheIdLookup.java @@ -0,0 +1,52 @@ +package io.ebeaninternal.api; + +import io.ebeaninternal.server.expression.IdInExpression; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Used for bean cache lookup with where ids in expression. + */ +public class CacheIdLookup { + + private final IdInExpression idInExpression; + + private int remaining; + + public CacheIdLookup(IdInExpression idInExpression) { + this.idInExpression = idInExpression; + } + + /** + * Return the Id values for the in expression. + */ + public Collection idValues() { + return idInExpression.idValues(); + } + + /** + * Process the hits returning the beans fetched from cache and + * adjusting the in expression (to not fetch the hits). + */ + public List removeHits(BeanCacheResult cacheResult) { + + Set hitIds = new HashSet<>(); + List beans = new ArrayList<>(hitIds.size()); + + for (BeanCacheResult.Entry hit : cacheResult.hits()) { + hitIds.add(hit.getKey()); + beans.add(hit.getBean()); + } + + this.remaining = idInExpression.removeIds(hitIds); + return beans; + } + + public boolean allHits() { + return remaining == 0; + } +} diff --git a/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java b/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java index 37ff3d2f2..30a1ff984 100644 --- a/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java +++ b/src/main/java/io/ebeaninternal/api/NaturalKeyQueryData.java @@ -69,18 +69,18 @@ public class NaturalKeyQueryData { /** * Match for IN expression. We only allow one IN clause. */ - public List matchIn(String propName, List sourceValues) { + public boolean matchIn(String propName, List inValues) { if (hasIn) { // only 1 IN allowed (to project naturalIds) - return null; + return false; } if (matchProperty(propName)) { this.hasIn = true; this.inProperty = propName; - this.inValues = new ArrayList<>(sourceValues); - return this.inValues; + this.inValues = inValues; + return true; } - return null; + return false; } /** @@ -198,21 +198,19 @@ public class NaturalKeyQueryData { 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); - - } else if (inPairs != null) { - Object naturalKey = hit.getKey(); - Pairs.Entry inValue = (Pairs.Entry)set.getInValue(naturalKey); - inPairs.remove(inValue); - } + removeKey(set.getInValue(hit.getKey())); beans.add(hit.getBean()); } - return beans; } + + private void removeKey(Object inValue) { + if (inValues != null) { + inValues.remove(inValue); + } else if (inPairs != null) { + //noinspection SuspiciousMethodCalls + inPairs.remove(inValue); + } + } } diff --git a/src/main/java/io/ebeaninternal/api/SpiQuery.java b/src/main/java/io/ebeaninternal/api/SpiQuery.java index 05f4c0e79..f162e950a 100644 --- a/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -429,6 +429,11 @@ public interface SpiQuery extends Query, TxnProfileEventCodes { */ void resetBeanCacheAutoMode(boolean findOne); + /** + * Bean cache lookup for find by ids. + */ + CacheIdLookup cacheIdLookup(); + /** * Collect natural key data for this query or null if the query does not match * the requirements of natural key lookup. diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 26b73d843..e1d35fcef 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -1071,14 +1071,21 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { SpiOrmQueryRequest createQueryRequest(Type type, Query query, Transaction t) { + SpiOrmQueryRequest request = buildQueryRequest(type, query, t); + request.prepareQuery(); + return request; + } + + SpiOrmQueryRequest buildQueryRequest(Type type, Query query, Transaction t) { + SpiQuery spiQuery = (SpiQuery) query; spiQuery.setType(type); spiQuery.checkNamedParameters(); - return createQueryRequest(spiQuery, t); + return buildQueryRequest(spiQuery, t); } - private SpiOrmQueryRequest createQueryRequest(SpiQuery query, Transaction t) { + private SpiOrmQueryRequest buildQueryRequest(SpiQuery query, Transaction t) { if (t == null) { t = currentServerTransaction(); @@ -1101,10 +1108,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { query.setOrigin(createCallStack()); } - OrmQueryRequest request = new OrmQueryRequest<>(this, queryEngine, query, (SpiTransaction) t); - request.prepareQuery(); - - return request; + return new OrmQueryRequest<>(this, queryEngine, query, (SpiTransaction) t); } /** @@ -1175,7 +1179,8 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } } - SpiOrmQueryRequest request = createQueryRequest(spiQuery, t); + SpiOrmQueryRequest request = buildQueryRequest(spiQuery, t); + request.prepareQuery(); if (request.isUseDocStore()) { return docStore().find(request); } @@ -1528,15 +1533,18 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { @SuppressWarnings("unchecked") private List findList(Query query, Transaction t, boolean findOne) { - SpiOrmQueryRequest request = createQueryRequest(Type.LIST, query, t); + SpiOrmQueryRequest request = buildQueryRequest(Type.LIST, query, t); request.resetBeanCacheAutoMode(findOne); + if ((t == null || !t.isSkipCache()) && request.getFromBeanCache()) { + // hit bean cache and got all results from cache + return request.getBeanCacheHits(); + } + + request.prepareQuery(); 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 af0f73443..7f6bb858f 100644 --- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -16,6 +16,7 @@ import io.ebean.event.BeanQueryAdapter; import io.ebean.text.json.JsonReadOptions; import io.ebeaninternal.api.BeanCacheResult; import io.ebeaninternal.api.CQueryPlanKey; +import io.ebeaninternal.api.CacheIdLookup; import io.ebeaninternal.api.HashQuery; import io.ebeaninternal.api.LoadContext; import io.ebeaninternal.api.NaturalKeyQueryData; @@ -208,7 +209,8 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery /** * Prepare the query and calculate the query plan key. */ - void prepareQuery() { + @Override + public void prepareQuery() { beanDescriptor.prepareQuery(query); adapterPreQuery(); this.secondaryQueries = query.convertJoins(); @@ -598,6 +600,14 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery // - merge the 2 results and return // + CacheIdLookup idLookup = query.cacheIdLookup(); + if (idLookup != null) { + BeanCacheResult cacheResult = beanDescriptor.cacheIdLookup(persistenceContext, idLookup.idValues()); + // adjust the query (IN clause) based on the cache hits + this.cacheBeans = idLookup.removeHits(cacheResult); + return idLookup.allHits(); + } + if (!beanDescriptor.isNaturalKeyCaching()) { 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 d4195701b..81a0f520f 100644 --- a/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -29,6 +29,11 @@ public interface SpiOrmQueryRequest extends BeanQueryRequest, DocQueryRequ */ BeanDescriptor getBeanDescriptor(); + /** + * Prepare the query for execution. + */ + void prepareQuery(); + /** * This will create a local (readOnly) transaction if no current transaction * exists. diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index ed600488a..f190d5e3c 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -1490,6 +1490,10 @@ public class BeanDescriptor implements BeanType, STreeType { return cacheHelp.beanCacheLoad(bean, ebi, cacheKey(id), context); } + public BeanCacheResult cacheIdLookup(PersistenceContext context, Collection ids) { + return cacheHelp.cacheIdLookup(context, ids); + } + /** * Use natural key lookup to hit the bean cache. */ diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index b2cb88ffb..398b4a31b 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -374,27 +374,30 @@ final class BeanDescriptorCacheHelp { return new CachedManyIds(idList); } -// BeanCacheResult cacheIdLookup(PersistenceContext context, Collection keys) { -// -// Set ids = new HashSet<>(keys); -// Map beanDataMap = beanCache.getAll(ids); -// if (beanLog.isTraceEnabled()) { -// beanLog.trace(" GET MANY {}({}) - hits:{}", cacheName, ids, beanDataMap.keySet()); -// } -// -// BeanCacheResult result = new BeanCacheResult<>(); -// // 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); -// result.add(bean, id); -// } -// -// return result; -// } + /** + * Hit the bean cache with the given ids returning the hits. + */ + BeanCacheResult cacheIdLookup(PersistenceContext context, Collection ids) { + + Set keys = new HashSet<>(ids.size()); + for (Object id : ids) { + keys.add(desc.cacheKey(id)); + } + + Map beanDataMap = beanCache.getAll(keys); + if (beanLog.isTraceEnabled()) { + beanLog.trace(" GET MANY {}({}) - hits:{}", cacheName, ids, beanDataMap.keySet()); + } + + BeanCacheResult result = new BeanCacheResult<>(); + for (Map.Entry entry : beanDataMap.entrySet()) { + CachedBeanData cachedBeanData = (CachedBeanData) entry.getValue(); + T bean = convertToBean(entry.getKey(), false, context, cachedBeanData); + result.add(bean, desc.getBeanId(bean)); + } + + return result; + } /** * Use natural keys to hit the bean cache and return resulting hits. diff --git a/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java b/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java index 533388153..e37e9d150 100644 --- a/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/IdInExpression.java @@ -9,20 +9,38 @@ import io.ebeaninternal.server.deploy.BeanDescriptor; import io.ebeaninternal.server.deploy.id.IdBinder; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; +import java.util.Set; /** * In a collection of Id values. */ public class IdInExpression extends NonPrepareExpression { - private final Collection idCollection; + private final List idCollection; private boolean multiValueIdSupported; public IdInExpression(Collection idCollection) { - this.idCollection = idCollection; + this.idCollection = new ArrayList<>(idCollection); + } + + /** + * Return the ids this expression is looking to fetch. + */ + public Collection idValues() { + return idCollection; + } + + /** + * Remove Ids that where obtained from l2 cache. Don't fetch these from DB. + */ + public int removeIds(Set hitIds) { + idCollection.removeAll(hitIds); + return idCollection.size(); } @Override diff --git a/src/main/java/io/ebeaninternal/server/expression/InExpression.java b/src/main/java/io/ebeaninternal/server/expression/InExpression.java index 656b32fe8..fa796f486 100644 --- a/src/main/java/io/ebeaninternal/server/expression/InExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/InExpression.java @@ -63,23 +63,25 @@ class InExpression extends AbstractExpression { return vals; } + private List initBindValues() { + if (bindValues == null) { + bindValues = values(); + } + return bindValues; + } + @Override public boolean naturalKey(NaturalKeyQueryData data) { // can't use naturalKey cache for NOT IN or when "empty" if (not || empty) { return false; } - List copy = data.matchIn(propName, bindValues); - if (copy == null) { - return false; - } - bindValues = copy; - return true; + return data.matchIn(propName, initBindValues()); } @Override public void prepareExpression(BeanQueryRequest request) { - bindValues = values(); + initBindValues(); if (bindValues.size() > 0) { multiValueSupported = request.isMultiValueSupported((bindValues.get(0)).getClass()); } @@ -146,9 +148,7 @@ class InExpression extends AbstractExpression { if (prop != null) { request.append(prop.getAssocIdInExpr(propName)); - String inClause = prop.getAssocIdInValueExpr(not, bindValues.size()); - request.append(inClause); - + request.append(prop.getAssocIdInValueExpr(not, bindValues.size())); } else { request.append(propName); request.appendInExpression(not, bindValues); diff --git a/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java b/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java index 07dbc2945..3dbbd10a1 100644 --- a/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java +++ b/src/main/java/io/ebeaninternal/server/expression/InPairsExpression.java @@ -54,7 +54,6 @@ class InPairsExpression extends AbstractExpression { @Override public void prepareExpression(BeanQueryRequest request) { - // at this stage translating pairs into varchar via DB concat multiValueSupported = request.isMultiValueSupported(String.class); } diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java b/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java index 644f236fe..90c4a1e72 100644 --- a/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java +++ b/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java @@ -60,7 +60,7 @@ class CQueryBindCapture { DQueryPlanOutput queryPlan = planLogger.logQueryPlan(request.getConnection(), cQueryPlan, last); if (queryPlan != null) { - queryPlan.with(queryTimeMicros, captureCount, cQueryPlan.getPlanKey().toString()); + queryPlan.with(queryTimeMicros, captureCount, cQueryPlan.getSqlHash()); request.process(queryPlan); } } diff --git a/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java b/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java index cb68aa7b2..4f7716ef3 100644 --- a/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java +++ b/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.query; +import io.ebean.ProfileLocation; import io.ebean.meta.MetaQueryPlan; /** @@ -9,29 +10,28 @@ class DQueryPlanOutput implements MetaQueryPlan { private final Class beanType; private final String label; - + private final ProfileLocation profileLocation; private final String sql; - private final String bind; - private final String plan; - private String planHash; + private String sqlHash; private long queryTimeMicros; private long captureCount; - DQueryPlanOutput(Class beanType, String label, String sql, String bind, String plan) { + DQueryPlanOutput(Class beanType, String label, String sql, String bind, String plan, ProfileLocation profileLocation) { this.beanType = beanType; this.label = label; this.sql = sql; this.bind = bind; this.plan = plan; + this.profileLocation = profileLocation; } @Override public String getQueryPlanHash() { - return planHash; + return sqlHash; } /** @@ -94,15 +94,15 @@ class DQueryPlanOutput implements MetaQueryPlan { @Override public String toString() { - return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName()) + " planHash:" + planHash + " label:" + label + " queryTimeMicros:" + queryTimeMicros + " captureCount:" + captureCount + "\n SQL:" + sql + "\nBIND:" + bind + "\nPLAN:" + plan; + return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName()) + " planHash:" + sqlHash + " label:" + label + " queryTimeMicros:" + queryTimeMicros + " captureCount:" + captureCount + "\n SQL:" + sql + "\nBIND:" + bind + "\nPLAN:" + plan; } /** * Additionally set the query execution time and the number of bind captures. */ - void with(long queryTimeMicros, long captureCount, String planHash) { + void with(long queryTimeMicros, long captureCount, String sqlHash) { this.queryTimeMicros = queryTimeMicros; this.captureCount = captureCount; - this.planHash = planHash; + this.sqlHash = sqlHash; } } diff --git a/src/main/java/io/ebeaninternal/server/query/QueryPlanLogger.java b/src/main/java/io/ebeaninternal/server/query/QueryPlanLogger.java index f3bbb267c..0455aaa5b 100644 --- a/src/main/java/io/ebeaninternal/server/query/QueryPlanLogger.java +++ b/src/main/java/io/ebeaninternal/server/query/QueryPlanLogger.java @@ -26,7 +26,7 @@ public abstract class QueryPlanLogger { } protected DQueryPlanOutput createPlan(CQueryPlan plan, String bind, String planString) { - return new DQueryPlanOutput(plan.getBeanType(), plan.getName(), plan.getSql(), bind, planString); + return new DQueryPlanOutput(plan.getBeanType(), plan.getName(), plan.getSql(), bind, planString, plan.getProfileLocation()); } DQueryPlanOutput readQueryPlanBasic(CQueryPlan plan, BindCapture bind, ResultSet rset) throws SQLException { diff --git a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index d79455f5d..e5e1639a7 100644 --- a/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -33,6 +33,7 @@ import io.ebean.event.readaudit.ReadEvent; import io.ebean.plugin.BeanType; import io.ebeaninternal.api.BindParams; import io.ebeaninternal.api.CQueryPlanKey; +import io.ebeaninternal.api.CacheIdLookup; import io.ebeaninternal.api.HashQuery; import io.ebeaninternal.api.ManyWhereJoins; import io.ebeaninternal.api.NaturalKeyQueryData; @@ -50,6 +51,7 @@ import io.ebeaninternal.server.deploy.BeanPropertyAssocMany; import io.ebeaninternal.server.deploy.InheritInfo; import io.ebeaninternal.server.deploy.TableJoin; import io.ebeaninternal.server.expression.DefaultExpressionList; +import io.ebeaninternal.server.expression.IdInExpression; import io.ebeaninternal.server.expression.SimpleExpression; import io.ebeaninternal.server.query.CancelableQuery; import io.ebeaninternal.server.query.NativeSqlQueryPlanKey; @@ -733,6 +735,21 @@ public class DefaultOrmQuery implements SpiQuery { return !manualId && !distinct && !singleAttribute; } + @Override + public CacheIdLookup cacheIdLookup() { + if (whereExpressions == null) { + return null; + } + List underlyingList = whereExpressions.getUnderlyingList(); + if (underlyingList.size() == 1) { + SpiExpression singleExpression = underlyingList.get(0); + if (singleExpression instanceof IdInExpression) { + return new CacheIdLookup<>((IdInExpression)singleExpression); + } + } + return null; + } + @Override public NaturalKeyQueryData naturalKey() { @@ -1848,8 +1865,7 @@ public class DefaultOrmQuery implements SpiQuery { @Override public void checkNamedParameters() { if (namedParams != null) { - Collection values = namedParams.values(); - for (ONamedParam value : values) { + for (ONamedParam value : namedParams.values()) { value.checkValueSet(); } } diff --git a/src/test/java/org/tests/cache/TestBeanCache.java b/src/test/java/org/tests/cache/TestBeanCache.java index ed9aae7ad..42af7612f 100644 --- a/src/test/java/org/tests/cache/TestBeanCache.java +++ b/src/test/java/org/tests/cache/TestBeanCache.java @@ -1,13 +1,21 @@ package org.tests.cache; import io.ebean.BaseTestCase; +import io.ebean.DB; import io.ebean.Ebean; +import io.ebean.cache.ServerCache; +import io.ebean.cache.ServerCacheStatistics; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.tests.model.basic.Country; import org.tests.model.basic.OCachedBean; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertNotNull; @@ -17,6 +25,10 @@ import static org.junit.Assert.assertNotNull; */ public class TestBeanCache extends BaseTestCase { + private static final Logger log = LoggerFactory.getLogger(TestBeanCache.class); + + private ServerCache beanCache = DB.getDefault().getServerCacheManager().getBeanCache(OCachedBean.class); + @Test public void findById_when_idTypeConverted() { @@ -40,7 +52,102 @@ public class TestBeanCache extends BaseTestCase { sql = LoggedSqlCollector.stop(); assertNotNull(bean2); assertThat(sql).isEmpty(); + } + @Test + public void idsInExpression() { + + List beans = createBeans(); + List ids = beans.stream().map(OCachedBean::getId).collect(Collectors.toList()); + + beanCache.clear(); + beanCache.getStatistics(true); + + LoggedSqlCollector.start(); + + log.info("All misses (0 of 3) ..."); + List list = DB.find(OCachedBean.class) + .where().idIn(ids) + .setUseCache(true) + .findList(); + + assertThat(list).hasSize(3); + assertBeanCacheHitMiss(0, 3); + List sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + if (isH2()) { + assertThat(sql.get(0)).contains("from o_cached_bean t0 where t0.id in (?, ?, ? )"); + } + + log.info("All hits (3 of 3) ..."); + list = DB.find(OCachedBean.class) + .where().idIn(ids) + .setUseCache(true) + .findList(); + + assertBeanCacheHitMiss(3, 0); + assertThat(list).hasSize(3); + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(0); // no misses + + // remove a bean so that we get a "partial" hit (2 out of 3 in cache) + beanCache.remove(beans.get(0).getId().toString()); + + log.info("Partial hits (2 of 3) ..."); + list = DB.find(OCachedBean.class) + .where().idIn(ids) + .setUseCache(true) + .findList(); + + assertBeanCacheHitMiss(2, 1); + assertThat(list).hasSize(3); + sql = LoggedSqlCollector.current(); + assertThat(sql).hasSize(1); + if (isH2()) { + // fetch the miss from DB + assertThat(sql.get(0)).contains("from o_cached_bean t0 where t0.id in (? )"); + } + + // remove beans so that we get a "partial" hit (1 out of 3 in cache) + beanCache.remove(beans.get(1).getId().toString()); + beanCache.remove(beans.get(2).getId().toString()); + + log.info("Partial hits (1 of 3) ..."); + list = DB.find(OCachedBean.class) + .where().idIn(ids) + .setUseCache(true) + .findList(); + + assertBeanCacheHitMiss(1, 2); + assertThat(list).hasSize(3); + sql = LoggedSqlCollector.stop(); + assertThat(sql).hasSize(1); + if (isH2()) { + // fetch the misses from DB + assertThat(sql.get(0)).contains("from o_cached_bean t0 where t0.id in (?, ? )"); + } + } + + private void assertBeanCacheHitMiss(int hitCount, int missCount) { + ServerCacheStatistics statistics = beanCache.getStatistics(true); + assertThat(statistics.getHitCount()).isEqualTo(hitCount); + assertThat(statistics.getMissCount()).isEqualTo(missCount); + } + + private List createBeans() { + + List names = Arrays.asList("z0", "z1", "z2"); + + List beans = new ArrayList<>(); + for (String name : names) { + OCachedBean bean = new OCachedBean(); + bean.setName(name); + beans.add(bean); + } + + DB.saveAll(beans); + + return beans; } @Test diff --git a/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java b/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java index b92949f6d..20935a319 100644 --- a/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java +++ b/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java @@ -68,7 +68,6 @@ public class PersonCacheTests { for (PersonCacheEmail email : emailList) { // get property that isn't in the cached data assertThat(email.getPersonInfo().getName()).isNotNull(); - System.out.println(email.getPersonInfo().getName()); } List sql = LoggedSqlCollector.current(); @@ -76,7 +75,9 @@ public class PersonCacheTests { assertThat(beanCacheInfo.getStatistics(true).getHitCount()).isEqualTo(3); - log.info("Fetch again ..."); + // force cache misses on PersonCacheEmail + beanCacheEmail.clear(); + log.info("Fetch again - cache missing on PersonCacheEmail and cache hits on PersonCacheInfo ..."); emailList = DB.find(PersonCacheEmail.class) @@ -84,7 +85,6 @@ public class PersonCacheTests { .setUseCache(true) .findList(); - for (PersonCacheEmail email : emailList) { // get property but it is in the cache data now System.out.println(email.getPersonInfo().getName());