diff --git a/pom.xml b/pom.xml index b39a81bf7..f6eb3b058 100644 --- a/pom.xml +++ b/pom.xml @@ -117,7 +117,7 @@ io.ebean ebean-annotation - 4.7 + 4.9 diff --git a/src/main/java/io/ebean/cache/ServerCacheManager.java b/src/main/java/io/ebean/cache/ServerCacheManager.java index 38de72fc6..a81fe9a52 100644 --- a/src/main/java/io/ebean/cache/ServerCacheManager.java +++ b/src/main/java/io/ebean/cache/ServerCacheManager.java @@ -1,5 +1,7 @@ package io.ebean.cache; +import java.util.List; + /** * The cache service for server side caching of beans and query results. */ @@ -14,6 +16,31 @@ public interface ServerCacheManager { */ boolean isLocalL2Caching(); + /** + * Return all the cache regions. + */ + List allRegions(); + + /** + * Set the regions that are enabled. + *

+ * Typically this is set on startup and at runtime (via dynamic configuration). + *

+ * + * @param regions A region name or comma delimited list of region names. + */ + void setEnabledRegions(String regions); + + /** + * Enable or disable all the cache regions. + */ + void setAllRegionsEnabled(boolean enabled); + + /** + * Return the cache region by name. Typically to enable or disable the region. + */ + ServerCacheRegion getRegion(String name); + /** * Return the cache for mapping natural keys to id values. */ @@ -46,7 +73,7 @@ public interface ServerCacheManager { /** * Clear all the local caches. - * + *

* This is used when the L2 Cache is based on clustered near-caches (Like Ebean-K8s-L2Cache). * It is not used when the L2 cache is a distributed cache such as HazelCast or Ignite etc. */ @@ -54,7 +81,7 @@ public interface ServerCacheManager { /** * Clear the local caches for this bean type. - * + *

* This is used when the L2 Cache is based on clustered near-caches (Like Ebean-K8s-L2Cache). * It is not used when the L2 cache is a distributed cache such as HazelCast or Ignite etc. */ diff --git a/src/main/java/io/ebean/cache/ServerCacheRegion.java b/src/main/java/io/ebean/cache/ServerCacheRegion.java new file mode 100644 index 000000000..b63668d91 --- /dev/null +++ b/src/main/java/io/ebean/cache/ServerCacheRegion.java @@ -0,0 +1,23 @@ +package io.ebean.cache; + +/** + * Cache region can be enabled independently. + */ +public interface ServerCacheRegion { + + /** + * Return the region name. + */ + String getName(); + + /** + * Return true if the cache region is enabled. + */ + boolean isEnabled(); + + /** + * Set to true to enable the cache region. + */ + void setEnabled(boolean enabled); + +} diff --git a/src/main/java/io/ebean/config/ServerConfig.java b/src/main/java/io/ebean/config/ServerConfig.java index 9efda763d..9b394a03e 100644 --- a/src/main/java/io/ebean/config/ServerConfig.java +++ b/src/main/java/io/ebean/config/ServerConfig.java @@ -479,6 +479,7 @@ public class ServerConfig { */ private boolean disableL2Cache; + private String enabledL2Regions; /** * Should the javax.validation.constraints.NotNull enforce a notNull column in DB. @@ -2914,6 +2915,7 @@ public class ServerConfig { collectQueryPlans = p.getBoolean("collectQueryPlans", collectQueryPlans); docStoreOnly = p.getBoolean("docStoreOnly", docStoreOnly); disableL2Cache = p.getBoolean("disableL2Cache", disableL2Cache); + enabledL2Regions = p.get("enabledL2Regions", enabledL2Regions); notifyL2CacheInForeground = p.getBoolean("notifyL2CacheInForeground", notifyL2CacheInForeground); explicitTransactionBeginMode = p.getBoolean("explicitTransactionBeginMode", explicitTransactionBeginMode); autoCommitMode = p.getBoolean("autoCommitMode", autoCommitMode); @@ -3136,6 +3138,20 @@ public class ServerConfig { this.expressionNativeIlike = expressionNativeIlike; } + /** + * Return the enabled L2 cache regions. + */ + public String getEnabledL2Regions() { + return enabledL2Regions; + } + + /** + * Set the enabled L2 cache regions (comma delimited). + */ + public void setEnabledL2Regions(String enabledL2Regions) { + this.enabledL2Regions = enabledL2Regions; + } + /** * Return true if L2 cache is disabled. */ 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/SpiCacheControl.java b/src/main/java/io/ebeaninternal/api/SpiCacheControl.java new file mode 100644 index 000000000..17e2fbe17 --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/SpiCacheControl.java @@ -0,0 +1,25 @@ +package io.ebeaninternal.api; + +public interface SpiCacheControl { + + /** + * Return true if bean caching or query caching. + */ + boolean isCaching(); + + /** + * Return true if bean caching. + */ + boolean isBeanCaching(); + + /** + * Return true if natural key caching. + */ + boolean isNaturalKeyCaching(); + + /** + * Return true if query caching. + */ + boolean isQueryCaching(); + +} diff --git a/src/main/java/io/ebeaninternal/api/SpiCacheRegion.java b/src/main/java/io/ebeaninternal/api/SpiCacheRegion.java new file mode 100644 index 000000000..78595b110 --- /dev/null +++ b/src/main/java/io/ebeaninternal/api/SpiCacheRegion.java @@ -0,0 +1,10 @@ +package io.ebeaninternal.api; + +import io.ebean.cache.ServerCacheRegion; + +/** + * Cache region can be enabled independently. + */ +public interface SpiCacheRegion extends ServerCacheRegion { + +} 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/cache/DefaultCacheAdapter.java b/src/main/java/io/ebeaninternal/server/cache/DefaultCacheAdapter.java index 0c3cfa9e5..37e89b864 100644 --- a/src/main/java/io/ebeaninternal/server/cache/DefaultCacheAdapter.java +++ b/src/main/java/io/ebeaninternal/server/cache/DefaultCacheAdapter.java @@ -2,6 +2,9 @@ package io.ebeaninternal.server.cache; import io.ebean.cache.ServerCache; import io.ebean.cache.ServerCacheManager; +import io.ebean.cache.ServerCacheRegion; + +import java.util.List; /** * Adapts SpiCacheManager to ServerCacheManager. @@ -22,6 +25,26 @@ public class DefaultCacheAdapter implements ServerCacheManager { return cacheManager.isLocalL2Caching(); } + @Override + public List allRegions() { + return cacheManager.allRegions(); + } + + @Override + public void setEnabledRegions(String regions) { + cacheManager.setEnabledRegions(regions); + } + + @Override + public ServerCacheRegion getRegion(String region) { + return cacheManager.getRegion(region); + } + + @Override + public void setAllRegionsEnabled(boolean enabled) { + cacheManager.setAllRegionsEnabled(enabled); + } + @Override public ServerCache getNaturalKeyCache(Class beanType) { return cacheManager.getNaturalKeyCache(beanType); diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java index bdb10d453..773846b79 100644 --- a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java +++ b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java @@ -1,14 +1,29 @@ package io.ebeaninternal.server.cache; import io.ebean.cache.ServerCache; +import io.ebean.cache.ServerCacheRegion; import io.ebean.cache.ServerCacheType; +import io.ebeaninternal.api.SpiCacheRegion; import io.ebeaninternal.server.cluster.ClusterManager; +import io.ebeaninternal.server.deploy.DCacheRegion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Manages the bean and query caches. */ public class DefaultServerCacheManager implements SpiCacheManager { + private static final Logger log = LoggerFactory.getLogger("io.ebean.cache.REGION"); + + private final Map regionMap = new HashMap<>(); + private final ClusterManager clusterManager; private final DefaultCacheHolder cacheHolder; @@ -39,6 +54,51 @@ public class DefaultServerCacheManager implements SpiCacheManager { return localL2Caching; } + @Override + public List allRegions() { + return new ArrayList<>(regionMap.values()); + } + + @Override + public void setEnabledRegions(String regions) { + if (regions != null) { + List enabledRegionNames = Arrays.asList(regions.split(",")); + + List disabled = new ArrayList<>(); + List enabled = new ArrayList<>(); + + for (SpiCacheRegion region : regionMap.values()) { + if (enabledRegionNames.contains(region.getName())) { + enabled.add(region.getName()); + if (!region.isEnabled()) { + region.setEnabled(true); + log.debug("Cache region[{}] enabled", region.getName()); + } + } else { + disabled.add(region.getName()); + if (region.isEnabled()) { + region.setEnabled(false); + log.debug("Cache region[{}] disabled", region.getName()); + } + } + } + log.info("Cache regions enabled:{} disabled:{}", enabled, disabled); + } + } + + @Override + public void setAllRegionsEnabled(boolean enabled) { + log.debug("All cache regions enabled[{}]", enabled); + for (SpiCacheRegion region : regionMap.values()) { + region.setEnabled(enabled); + } + } + + @Override + public SpiCacheRegion getRegion(String region) { + return regionMap.computeIfAbsent(region, DCacheRegion::new); + } + /** * Clear all caches. */ diff --git a/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java b/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java index 3e83203bd..5ef06b991 100644 --- a/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java +++ b/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java @@ -1,6 +1,10 @@ package io.ebeaninternal.server.cache; import io.ebean.cache.ServerCache; +import io.ebean.cache.ServerCacheRegion; +import io.ebeaninternal.api.SpiCacheRegion; + +import java.util.List; /** * The cache service for server side caching of beans and query results. @@ -16,6 +20,28 @@ public interface SpiCacheManager { */ boolean isLocalL2Caching(); + /** + * Return all the regions. + */ + List allRegions(); + + /** + * Set the regions that are enabled. + * + * @param regions A single region name or a comma delimited list of region names. + */ + void setEnabledRegions(String regions); + + /** + * Enable or disable all cache regions. + */ + void setAllRegionsEnabled(boolean enabled); + + /** + * Return the cache region. We can enable L2 caching by region. + */ + SpiCacheRegion getRegion(String region); + /** * Return the cache for mapping natural keys to id values. */ diff --git a/src/main/java/io/ebeaninternal/server/core/CacheOptions.java b/src/main/java/io/ebeaninternal/server/core/CacheOptions.java index 24d441828..b2ef8073c 100644 --- a/src/main/java/io/ebeaninternal/server/core/CacheOptions.java +++ b/src/main/java/io/ebeaninternal/server/core/CacheOptions.java @@ -12,45 +12,70 @@ public class CacheOptions { */ public static final CacheOptions NO_CACHING = new CacheOptions(); - public static final CacheOptions INVALIDATE_QUERY_CACHE = new CacheOptions(true); + private static final String R0 = "r0"; + + private static final CacheOptions INVALIDATE_QUERY_CACHE_R0 = new CacheOptions(true); private final boolean invalidateQueryCache; private final boolean enableBeanCache; private final boolean enableQueryCache; private final boolean readOnly; private final String[] naturalKey; + private final String region; + + public static CacheOptions invalidateQueryCache(String region) { + if (R0.equals(region)) { + return INVALIDATE_QUERY_CACHE_R0; + } else { + return new CacheOptions(true, region); + } + } /** * Construct for no caching. */ private CacheOptions() { - invalidateQueryCache = false; - enableBeanCache = false; - enableQueryCache = false; - readOnly = false; - naturalKey = null; + this.invalidateQueryCache = false; + this.enableBeanCache = false; + this.enableQueryCache = false; + this.readOnly = false; + this.naturalKey = null; + this.region = null; } /** * Construct for invalidateQueryCache. */ private CacheOptions(boolean invalidateQueryCache) { + this(invalidateQueryCache, R0); + } + + private CacheOptions(boolean invalidateQueryCache, String region) { this.invalidateQueryCache = invalidateQueryCache; - enableBeanCache = false; - enableQueryCache = false; - readOnly = false; - naturalKey = null; + this.enableBeanCache = false; + this.enableQueryCache = false; + this.readOnly = false; + this.naturalKey = null; + this.region = region; } /** * Construct with cache annotation. */ public CacheOptions(Cache cache, String[] naturalKey) { - invalidateQueryCache = false; - enableBeanCache = cache.enableBeanCache(); - enableQueryCache = cache.enableQueryCache(); - readOnly = cache.readOnly(); + this.invalidateQueryCache = false; + this.enableBeanCache = cache.enableBeanCache(); + this.enableQueryCache = cache.enableQueryCache(); + this.readOnly = cache.readOnly(); this.naturalKey = naturalKey; + this.region = cache.region(); + } + + /** + * Return the cache region name. + */ + public String getRegion() { + return region; } /** diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 4606d7fde..e1d35fcef 100644 --- a/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -446,6 +446,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { if (encryptKeyManager != null) { encryptKeyManager.initialise(); } + serverCacheManager.setEnabledRegions(serverConfig.getEnabledL2Regions()); } /** @@ -484,7 +485,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { */ private void shutdownInternal(boolean shutdownDataSource, boolean deregisterDriver) { - logger.debug("Shutting down EbeanServer {}", serverName); + logger.debug("Shutting down instance:{}", serverName); if (shutdown) { // already shutdown return; @@ -790,23 +791,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { transactionManager.exitScopedTransaction(returnOrThrowable, opCode); } - /** - * Returns the current transaction (or null) from the scope. - */ @Override public SpiTransaction currentServerTransaction() { return transactionManager.getActive(); } - /** - * Start a transaction with 'REQUIRED' semantics. - *

- * If a transaction already exists that transaction will be used. - *

- *

- * Note that the transaction is stored in a ThreadLocal variable. - *

- */ @Override public Transaction beginTransaction() { return beginTransaction(TxScope.required()); @@ -817,12 +806,6 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return transactionManager.beginScopedTransaction(txScope); } - /** - * Start a transaction with a specific Isolation Level. - *

- * Note that the transaction is stored in a ThreadLocal variable. - *

- */ @Override public Transaction beginTransaction(TxIsolation isolation) { // start an explicit transaction @@ -837,10 +820,6 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return t; } - /** - * Return the current transaction or null if there is not one currently in - * scope. - */ @Override public Transaction currentTransaction() { return transactionManager.getActive(); @@ -851,50 +830,16 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { currentTransaction().flush(); } - /** - * Commit the current transaction. - */ @Override public void commitTransaction() { currentTransaction().commit(); } - /** - * Rollback the current transaction. - */ @Override public void rollbackTransaction() { currentTransaction().rollback(); } - /** - * If the current transaction has already been committed do nothing otherwise - * rollback the transaction. - *

- * Useful to put in a finally block to ensure the transaction is ended, rather - * than a rollbackTransaction() in each catch block. - *

- *

- * Code example:
- *

- *

-   * <code>
-   * Ebean.startTransaction();
-   * try {
-   * 	// do some fetching and or persisting
-   *
-   * 	// commit at the end
-   * 	Ebean.commitTransaction();
-   *
-   * } finally {
-   * 	// if commit didn't occur then rollback the transaction
-   * 	Ebean.endTransaction();
-   * }
-   * </code>
-   * 
- *

- *

- */ @Override public void endTransaction() { Transaction transaction = transactionManager.getInScope(); @@ -903,13 +848,6 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { } } - /** - * return the next unique identity value. - *

- * Uses the BeanDescriptor deployment information to determine the sequence to - * use. - *

- */ @Override public Object nextId(Class beanType) { BeanDescriptor desc = getBeanDescriptor(beanType); @@ -1133,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(); @@ -1163,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); } /** @@ -1237,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); } @@ -1590,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 88c816040..398b4a31b 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -7,6 +7,8 @@ import io.ebean.bean.PersistenceContext; import io.ebean.cache.QueryCacheEntry; import io.ebean.cache.ServerCache; import io.ebeaninternal.api.BeanCacheResult; +import io.ebeaninternal.api.SpiCacheControl; +import io.ebeaninternal.api.SpiCacheRegion; import io.ebeaninternal.api.SpiTransaction; import io.ebeaninternal.api.TransactionEventTable.TableIUD; import io.ebeaninternal.server.cache.CacheChangeSet; @@ -72,6 +74,9 @@ final class BeanDescriptorCacheHelp { private final boolean noCaching; + private final SpiCacheControl cacheControl; + private final SpiCacheRegion cacheRegion; + /** * Set to true if all persist changes need to notify the cache. */ @@ -113,6 +118,13 @@ final class BeanDescriptorCacheHelp { this.naturalKeyCache = null; } this.noCaching = (beanCache == null && queryCache == null); + if (noCaching) { + this.cacheControl = DCacheControlNone.INSTANCE; + this.cacheRegion = (invalidateQueryCache) ? cacheManager.getRegion(cacheOptions.getRegion()) : DCacheRegionNone.INSTANCE; + } else { + this.cacheRegion = cacheManager.getRegion(cacheOptions.getRegion()); + this.cacheControl = new DCacheControl(cacheRegion, (beanCache != null), (naturalKeyCache != null), (queryCache != null)); + } } /** @@ -123,8 +135,8 @@ final class BeanDescriptorCacheHelp { cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes(); if (logger.isDebugEnabled()) { - if (isBeanCaching() || isQueryCaching() || cacheNotifyOnAll || cacheNotifyOnDelete) { - String notifyMode = cacheNotifyOnAll ? "All" : (cacheNotifyOnDelete ? "Delete" : "None"); + if (cacheNotifyOnAll || cacheNotifyOnDelete) { + String notifyMode = cacheNotifyOnAll ? "All" : "Delete"; logger.debug("l2 caching on {} - beanCaching:{} queryCaching:{} notifyMode:{} ", desc.getFullName(), isBeanCaching(), isQueryCaching(), notifyMode); } @@ -148,36 +160,36 @@ final class BeanDescriptorCacheHelp { * Return true if the persist request needs to notify the cache. */ boolean isCacheNotify(PersistRequest.Type type) { - return cacheNotifyOnAll - || cacheNotifyOnDelete && (type == PersistRequest.Type.DELETE || type == PersistRequest.Type.DELETE_PERMANENT); + return cacheRegion.isEnabled() + && (cacheNotifyOnAll || cacheNotifyOnDelete && (type == PersistRequest.Type.DELETE || type == PersistRequest.Type.DELETE_PERMANENT)); } /** * Return true if there is currently query caching for this type of bean. */ boolean isQueryCaching() { - return queryCache != null; + return cacheControl.isQueryCaching(); } /** * Return true if there is currently bean caching for this type of bean. */ boolean isBeanCaching() { - return beanCache != null; + return cacheControl.isBeanCaching(); } /** * Return true if there is natural key caching for this type of bean. */ boolean isNaturalKeyCaching() { - return naturalKeyCache != null; + return cacheControl.isNaturalKeyCaching(); } /** * Return true if there is bean or query caching on this type. */ boolean isCaching() { - return beanCache != null || queryCache != null; + return cacheControl.isCaching(); } /** @@ -206,7 +218,7 @@ final class BeanDescriptorCacheHelp { /** * Add query cache clear to the changeSet. */ - void queryCacheClear(CacheChangeSet changeSet) { + private void queryCacheClear(CacheChangeSet changeSet) { if (queryCache != null) { changeSet.addClearQuery(desc); } @@ -292,8 +304,8 @@ final class BeanDescriptorCacheHelp { return false; } - Object ownerBean = bc.getOwnerBean(); - EntityBeanIntercept ebi = ((EntityBean) ownerBean)._ebean_getIntercept(); + EntityBean ownerBean = bc.getOwnerBean(); + EntityBeanIntercept ebi = ownerBean._ebean_getIntercept(); PersistenceContext persistenceContext = ebi.getPersistenceContext(); BeanDescriptor targetDescriptor = many.getTargetDescriptor(); @@ -362,6 +374,31 @@ final class BeanDescriptorCacheHelp { return new CachedManyIds(idList); } + /** + * 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/deploy/DCacheControl.java b/src/main/java/io/ebeaninternal/server/deploy/DCacheControl.java new file mode 100644 index 000000000..a99dabf89 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/DCacheControl.java @@ -0,0 +1,42 @@ +package io.ebeaninternal.server.deploy; + +import io.ebeaninternal.api.SpiCacheControl; +import io.ebeaninternal.api.SpiCacheRegion; + +/** + * Default implementation of SpiCacheControl. + */ +class DCacheControl implements SpiCacheControl { + + private final SpiCacheRegion region; + private final boolean bean; + private final boolean naturalKey; + private final boolean query; + + DCacheControl(SpiCacheRegion region, boolean bean, boolean naturalKey, boolean query) { + this.region = region; + this.bean = bean; + this.naturalKey = naturalKey; + this.query = query; + } + + @Override + public boolean isCaching() { + return (bean || query) && region.isEnabled(); + } + + @Override + public boolean isBeanCaching() { + return bean && region.isEnabled(); + } + + @Override + public boolean isNaturalKeyCaching() { + return naturalKey && region.isEnabled(); + } + + @Override + public boolean isQueryCaching() { + return query && region.isEnabled(); + } +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/DCacheControlNone.java b/src/main/java/io/ebeaninternal/server/deploy/DCacheControlNone.java new file mode 100644 index 000000000..8f4797f47 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/DCacheControlNone.java @@ -0,0 +1,31 @@ +package io.ebeaninternal.server.deploy; + +import io.ebeaninternal.api.SpiCacheControl; + +/** + * Cache control used when no caching is on the bean type. + */ +class DCacheControlNone implements SpiCacheControl { + + static final SpiCacheControl INSTANCE = new DCacheControlNone(); + + @Override + public boolean isCaching() { + return false; + } + + @Override + public boolean isBeanCaching() { + return false; + } + + @Override + public boolean isNaturalKeyCaching() { + return false; + } + + @Override + public boolean isQueryCaching() { + return false; + } +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/DCacheRegion.java b/src/main/java/io/ebeaninternal/server/deploy/DCacheRegion.java new file mode 100644 index 000000000..2056833b7 --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/DCacheRegion.java @@ -0,0 +1,29 @@ +package io.ebeaninternal.server.deploy; + +import io.ebeaninternal.api.SpiCacheRegion; + +public class DCacheRegion implements SpiCacheRegion { + + private boolean enabled = true; + + private final String name; + + public DCacheRegion(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/src/main/java/io/ebeaninternal/server/deploy/DCacheRegionNone.java b/src/main/java/io/ebeaninternal/server/deploy/DCacheRegionNone.java new file mode 100644 index 000000000..50c9ba33a --- /dev/null +++ b/src/main/java/io/ebeaninternal/server/deploy/DCacheRegionNone.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.deploy; + +import io.ebeaninternal.api.SpiCacheRegion; + +class DCacheRegionNone implements SpiCacheRegion { + + static SpiCacheRegion INSTANCE = new DCacheRegionNone(); + + @Override + public String getName() { + return ""; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public void setEnabled(boolean enabled) { + throw new IllegalStateException("Not expected"); + } +} 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 b43b0d6fc..bf51ceea6 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -473,8 +473,8 @@ public class DeployBeanDescriptor { /** * Set that this type invalidates query caches. */ - public void setInvalidateQueryCache() { - this.cacheOptions = CacheOptions.INVALIDATE_QUERY_CACHE; + public void setInvalidateQueryCache(String region) { + this.cacheOptions = CacheOptions.invalidateQueryCache(region); } /** diff --git a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java index 657c94797..a04b8db5e 100644 --- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java +++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java @@ -202,13 +202,12 @@ public class AnnotationClass extends AnnotationParser { } else { InvalidateQueryCache invalidateQueryCache = AnnotationUtil.findAnnotationRecursive(cls, InvalidateQueryCache.class); if (invalidateQueryCache != null) { - descriptor.setInvalidateQueryCache(); + descriptor.setInvalidateQueryCache(invalidateQueryCache.region()); } } } - Set namedQueries = AnnotationUtil.findAnnotationsRecursive(cls, NamedQuery.class); - for (NamedQuery namedQuery : namedQueries) { + for (NamedQuery namedQuery : AnnotationUtil.findAnnotationsRecursive(cls, NamedQuery.class)) { descriptor.addNamedQuery(namedQuery.name(), namedQuery.query()); } } 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/io/ebean/BaseTestCase.java b/src/test/java/io/ebean/BaseTestCase.java index 17dc0efc1..ee10521e9 100644 --- a/src/test/java/io/ebean/BaseTestCase.java +++ b/src/test/java/io/ebean/BaseTestCase.java @@ -48,7 +48,7 @@ public abstract class BaseTestCase { DB_CLOCK_DELTA = 100; } logger.debug("... preStart"); - if (!AgentLoader.loadAgentFromClasspath("ebean-agent", "debug=1")) { + if (!AgentLoader.loadAgentByMainClass("io.ebean.enhance.Transformer", "debug=1")) { logger.info("avaje-ebeanorm-agent not found in classpath - not dynamically loaded"); } try { diff --git a/src/test/java/io/ebean/cache/ServerCacheManagerTest.java b/src/test/java/io/ebean/cache/ServerCacheManagerTest.java new file mode 100644 index 000000000..337a80116 --- /dev/null +++ b/src/test/java/io/ebean/cache/ServerCacheManagerTest.java @@ -0,0 +1,48 @@ +package io.ebean.cache; + +import io.ebean.DB; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ServerCacheManagerTest { + + @Test + public void test() { + + ServerCacheManager cacheManager = DB.getDefault().getServerCacheManager(); + + for (ServerCacheRegion region : cacheManager.allRegions()) { + assertTrue(region.isEnabled()); + } + + cacheManager.setEnabledRegions("r0,doesNotExist"); + assertThat(cacheManager.getRegion("email").isEnabled()).isFalse(); + assertThat(cacheManager.getRegion("r0").isEnabled()).isTrue(); + + cacheManager.setEnabledRegions("r0"); + assertThat(cacheManager.getRegion("email").isEnabled()).isFalse(); + assertThat(cacheManager.getRegion("r0").isEnabled()).isTrue(); + + cacheManager.setEnabledRegions(null); + assertThat(cacheManager.getRegion("email").isEnabled()).isFalse(); + assertThat(cacheManager.getRegion("r0").isEnabled()).isTrue(); + + cacheManager.setEnabledRegions("email"); + assertThat(cacheManager.getRegion("email").isEnabled()).isTrue(); + assertThat(cacheManager.getRegion("r0").isEnabled()).isFalse(); + + cacheManager.setAllRegionsEnabled(false); + for (ServerCacheRegion region : cacheManager.allRegions()) { + assertFalse(region.isEnabled()); + } + + cacheManager.setAllRegionsEnabled(true); + for (ServerCacheRegion region : cacheManager.allRegions()) { + assertTrue(region.isEnabled()); + } + + } +} diff --git a/src/test/java/io/ebean/config/ServerConfigTest.java b/src/test/java/io/ebean/config/ServerConfigTest.java index e5487b2b9..52965604d 100644 --- a/src/test/java/io/ebean/config/ServerConfigTest.java +++ b/src/test/java/io/ebean/config/ServerConfigTest.java @@ -68,6 +68,7 @@ public class ServerConfigTest { props.setProperty("mappingLocations", "classpath:/foo;bar"); props.setProperty("namingConvention", "io.ebean.config.MatchingNamingConvention"); props.setProperty("idGeneratorAutomatic", "true"); + props.setProperty("enabledL2Regions", "r0,users,orgs"); serverConfig.loadFromProperties(props); @@ -85,6 +86,7 @@ public class ServerConfigTest { assertEquals(PersistBatch.ALL, serverConfig.getPersistBatchOnCascade()); assertEquals(PlatformConfig.DbUuid.BINARY, serverConfig.getPlatformConfig().getDbUuid()); assertEquals(JsonConfig.DateTime.ISO8601, serverConfig.getJsonDateTime()); + assertEquals("r0,users,orgs", serverConfig.getEnabledL2Regions()); assertEquals(42, serverConfig.getJdbcFetchSizeFindEach()); assertEquals(43, serverConfig.getJdbcFetchSizeFindList()); @@ -111,6 +113,9 @@ public class ServerConfigTest { assertEquals(PersistBatch.ALL, serverConfig.getPersistBatch()); assertEquals(PersistBatch.ALL, serverConfig.getPersistBatchOnCascade()); + + serverConfig.setEnabledL2Regions("r0,orgs"); + assertEquals("r0,orgs", serverConfig.getEnabledL2Regions()); } @Test 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/PersonCacheEmail.java b/src/test/java/org/tests/cache/personinfo/PersonCacheEmail.java index e78463fe9..58ddea0a2 100644 --- a/src/test/java/org/tests/cache/personinfo/PersonCacheEmail.java +++ b/src/test/java/org/tests/cache/personinfo/PersonCacheEmail.java @@ -8,7 +8,7 @@ import javax.persistence.ManyToOne; import javax.validation.constraints.Size; @Entity -@Cache +@Cache(region = "email") public class PersonCacheEmail { @Id diff --git a/src/test/java/org/tests/cache/personinfo/PersonCacheInfo.java b/src/test/java/org/tests/cache/personinfo/PersonCacheInfo.java index bc24ce960..25d8e4d51 100644 --- a/src/test/java/org/tests/cache/personinfo/PersonCacheInfo.java +++ b/src/test/java/org/tests/cache/personinfo/PersonCacheInfo.java @@ -7,7 +7,7 @@ import javax.persistence.Id; import javax.validation.constraints.Size; @Entity -@Cache +@Cache(region = "email") public class PersonCacheInfo { @Id diff --git a/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java b/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java index 5cc192eca..20935a319 100644 --- a/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java +++ b/src/test/java/org/tests/cache/personinfo/PersonCacheTests.java @@ -1,17 +1,31 @@ package org.tests.cache.personinfo; -import io.ebean.Ebean; +import io.ebean.DB; import io.ebean.cache.ServerCache; +import io.ebean.cache.ServerCacheRegion; +import io.ebean.cache.ServerCacheStatistics; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; public class PersonCacheTests { + private static final Logger log = LoggerFactory.getLogger(PersonCacheTests.class); + + private final List ids = Arrays.asList("E001", "E002", "E003"); + + private ServerCache beanCacheInfo = DB.getDefault().getServerCacheManager().getBeanCache(PersonCacheInfo.class); + private ServerCache beanCacheEmail = DB.getDefault().getServerCacheManager().getBeanCache(PersonCacheEmail.class); + + private ServerCacheRegion region = DB.getDefault().getServerCacheManager().getRegion("email"); + private void insert(int id, String email) { PersonCacheInfo person = new PersonCacheInfo("P00" + id, "P00" + id); @@ -19,8 +33,8 @@ public class PersonCacheTests { PersonCacheEmail personEmail = new PersonCacheEmail("E00" + id, email); personEmail.setPersonInfo(person); - Ebean.save(person); - Ebean.save(personEmail); + DB.save(person); + DB.save(personEmail); } private void addTestData() { @@ -37,18 +51,15 @@ public class PersonCacheTests { LoggedSqlCollector.start(); - Ebean.find(PersonCacheInfo.class) + DB.find(PersonCacheInfo.class) .select("personId") // do not fetch name .setUseCache(true) .findList(); - ServerCache beanCache = Ebean.getDefaultServer().getServerCacheManager().getBeanCache(PersonCacheInfo.class); - beanCache.getStatistics(true); - - List ids = Arrays.asList(new String[]{"E001", "E002", "E003"}); + beanCacheInfo.getStatistics(true); List emailList = - Ebean.find(PersonCacheEmail.class) + DB.find(PersonCacheEmail.class) .where().idIn(ids) .setUseCache(true) .findList(); @@ -57,23 +68,23 @@ 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(); assertThat(sql).hasSize(3); - assertThat(beanCache.getStatistics(true).getHitCount()).isEqualTo(3); + assertThat(beanCacheInfo.getStatistics(true).getHitCount()).isEqualTo(3); - System.out.println("Fetch again ..."); + // force cache misses on PersonCacheEmail + beanCacheEmail.clear(); + log.info("Fetch again - cache missing on PersonCacheEmail and cache hits on PersonCacheInfo ..."); emailList = - Ebean.find(PersonCacheEmail.class) + DB.find(PersonCacheEmail.class) .where().idIn(ids) .setUseCache(true) .findList(); - for (PersonCacheEmail email : emailList) { // get property but it is in the cache data now System.out.println(email.getPersonInfo().getName()); @@ -83,7 +94,45 @@ public class PersonCacheTests { sql = LoggedSqlCollector.stop(); assertThat(sql).hasSize(1); - assertThat(beanCache.getStatistics(true).getHitCount()).isEqualTo(3); + assertThat(beanCacheInfo.getStatistics(true).getHitCount()).isEqualTo(3); + + turnOffRegion_expect_noCacheUse(); } + + private void turnOffRegion_expect_noCacheUse() { + + assertTrue(region.isEnabled()); + beanCacheEmail.getStatistics(true); + + log.info("Disabled region ..."); + region.setEnabled(false); + + for (Object id : ids) { + DB.find(PersonCacheEmail.class) + .where().idEq(id) + .setUseCache(true) + .findOne(); + } + + // assert that we didn't hit the cache + ServerCacheStatistics statistics = beanCacheEmail.getStatistics(true); + assertThat(statistics.getHitCount()).isEqualTo(0); + assertThat(statistics.getMissCount()).isEqualTo(0); + + log.info("Enabled region ..."); + region.setEnabled(true); + + for (Object id : ids) { + DB.find(PersonCacheEmail.class) + .where().idEq(id) + .setUseCache(true) + .findOne(); + } + + // assert that we DID hit the cache + statistics = beanCacheEmail.getStatistics(true); + assertThat(statistics.getHitCount()).isEqualTo(3); + assertThat(statistics.getMissCount()).isEqualTo(0); + } } diff --git a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java index d1a95de0b..c9352f5fc 100644 --- a/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java +++ b/src/test/java/org/tests/model/basic/cache/TestCacheViaComplexNaturalKey.java @@ -8,6 +8,8 @@ import io.ebean.cache.ServerCacheManager; import io.ebean.cache.ServerCacheStatistics; import org.ebeantest.LoggedSqlCollector; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; @@ -17,6 +19,8 @@ import static org.assertj.core.api.Assertions.assertThat; public class TestCacheViaComplexNaturalKey extends BaseTestCase { + private static final Logger log = LoggerFactory.getLogger(TestCacheViaComplexNaturalKey.class); + private ServerCache beanCache = cacheManager().getBeanCache(OCachedNatKeyBean.class); private ServerCache natKeyCache = cacheManager().getNaturalKeyCache(OCachedNatKeyBean.class); @@ -29,11 +33,8 @@ public class TestCacheViaComplexNaturalKey extends BaseTestCase { 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) { + for (String store : Arrays.asList("abc", "def")) { + for (String sku : Arrays.asList("1", "2", "3", "4")) { saveBean(store, sku); } } @@ -92,10 +93,11 @@ public class TestCacheViaComplexNaturalKey extends BaseTestCase { loadSomeIntoCache(); String storeId = "abc"; - List skus = new ArrayList<>(Arrays.asList("1", "2", "3")); + List skus = Arrays.asList("1", "2", "3"); LoggedSqlCollector.start(); + log.info("Partial (2 of 3) ..."); List list = Ebean.find(OCachedNatKeyBean.class) .where() .eq("store", storeId) @@ -104,7 +106,7 @@ public class TestCacheViaComplexNaturalKey extends BaseTestCase { .orderBy("sku desc") .findList(); - List sql = LoggedSqlCollector.stop(); + List sql = LoggedSqlCollector.current(); assertThat(sql).hasSize(1); if (isH2()) { // in clause with only 1 bind param - (sku=3 ... we got hits on sku 1 and 2) @@ -112,9 +114,45 @@ public class TestCacheViaComplexNaturalKey extends BaseTestCase { } assertThat(list).hasSize(3); - assertNaturalKeyHitMiss(2, 1); assertBeanCacheHitMiss(2, 0); + + list = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", storeId) + .in("sku", skus) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + sql = LoggedSqlCollector.current(); + + assertThat(list).hasSize(3); + assertThat(sql).hasSize(0); + assertNaturalKeyHitMiss(3, 0); + assertBeanCacheHitMiss(3, 0); + + natKeyCache.remove("abc;1;"); + natKeyCache.remove("abc;3;"); + + log.info("Partial (1 of 3) ..."); + list = Ebean.find(OCachedNatKeyBean.class) + .where() + .eq("store", storeId) + .in("sku", skus) + .setUseCache(true) + .orderBy("sku desc") + .findList(); + + sql = LoggedSqlCollector.stop(); + + assertThat(list).hasSize(3); + assertThat(sql).hasSize(1); + if (isH2()) { + assertThat(sql.get(0)).contains("where t0.store = ? and t0.sku in (?, ? ) order by t0.sku desc; --bind(abc,Array[2]={1,3})"); + } + assertNaturalKeyHitMiss(1, 2); + assertBeanCacheHitMiss(1, 0); } @Test diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 7236805d5..82935fc18 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -86,12 +86,13 @@ - + + - - - - + + + +