touchedTables() {
+ return touchedTables;
}
/**
@@ -40,7 +44,7 @@ public class CacheChangeSet {
*
* Return the set of table changes to process invalidation for entities based on views.
*/
- public Set apply() {
+ public void apply() {
for (BeanDescriptor> entry : queryCaches) {
entry.clearQueryCache();
}
@@ -50,7 +54,13 @@ public class CacheChangeSet {
for (CacheChange entry : manyChangeMap.values()) {
entry.apply();
}
- return viewInvalidation;
+ }
+
+ /**
+ * Add an entry to clear a query cache.
+ */
+ public void addInvalidate(BeanDescriptor> descriptor) {
+ touchedTables.add(descriptor.getBaseTable());
}
/**
@@ -85,29 +95,23 @@ public class CacheChangeSet {
* On bean insert register table for view based entity invalidation.
*/
public void addBeanInsert(String baseTable) {
- if (viewEntityInvalidation) {
- viewInvalidation.add(baseTable);
- }
+ touchedTables.add(baseTable);
}
/**
* Remove a bean from the cache.
*/
public void addBeanRemove(BeanDescriptor desc, Object id) {
+ touchedTables.add(desc.getBaseTable());
entries.add(new CacheChangeBeanRemove(desc, id));
- if (viewEntityInvalidation) {
- viewInvalidation.add(desc.getBaseTable());
- }
}
/**
* Update a bean entry.
*/
public void addBeanUpdate(BeanDescriptor desc, Object id, Map changes, boolean updateNaturalKey, long version) {
+ touchedTables.add(desc.getBaseTable());
entries.add(new CacheChangeBeanUpdate(desc, id, changes, updateNaturalKey, version));
- if (viewEntityInvalidation) {
- viewInvalidation.add(desc.getBaseTable());
- }
}
/**
@@ -125,6 +129,13 @@ public class CacheChangeSet {
return manyChangeMap.computeIfAbsent(key, ManyChange::new);
}
+ /**
+ * Return the modification timestamp for these changes.
+ */
+ public long modificationTimestamp() {
+ return modificationTimestamp;
+ }
+
/**
* Changes for a specific many property.
*/
diff --git a/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java b/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
index b61b48080..36a7ed571 100644
--- a/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
+++ b/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.cache;
+import io.ebean.cache.QueryCacheEntryValidate;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.config.CurrentTenantProvider;
@@ -19,6 +20,8 @@ public class CacheManagerOptions {
private CurrentTenantProvider currentTenantProvider;
+ private QueryCacheEntryValidate queryCacheEntryValidate;
+
private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
private ServerCacheOptions beanDefault = new ServerCacheOptions();
private ServerCacheOptions queryDefault = new ServerCacheOptions();
@@ -45,8 +48,9 @@ public class CacheManagerOptions {
return this;
}
- public CacheManagerOptions with(ServerCacheFactory cacheFactory) {
+ public CacheManagerOptions with(ServerCacheFactory cacheFactory, QueryCacheEntryValidate queryCacheEntryValidate) {
this.cacheFactory = cacheFactory;
+ this.queryCacheEntryValidate = queryCacheEntryValidate;
return this;
}
@@ -82,4 +86,8 @@ public class CacheManagerOptions {
public ClusterManager getClusterManager() {
return clusterManager;
}
+
+ public QueryCacheEntryValidate getQueryCacheEntryValidate() {
+ return queryCacheEntryValidate;
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java b/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
index 092fe7b9a..20e966f6e 100644
--- a/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
+++ b/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java
@@ -2,7 +2,9 @@ package io.ebeaninternal.server.cache;
import io.ebean.annotation.CacheBeanTuning;
import io.ebean.annotation.CacheQueryTuning;
+import io.ebean.cache.QueryCacheEntryValidate;
import io.ebean.cache.ServerCache;
+import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
@@ -33,22 +35,14 @@ class DefaultCacheHolder {
private final CurrentTenantProvider tenantProvider;
- DefaultCacheHolder(CacheManagerOptions builder) {
- this(builder.getCacheFactory(), builder.getBeanDefault(), builder.getQueryDefault(), builder.getCurrentTenantProvider());
- }
+ private final QueryCacheEntryValidate queryCacheEntryValidate;
- /**
- * Create with a cache factory and default cache options.
- *
- * @param cacheFactory the factory for creating the cache
- * @param beanDefault the default options for tuning bean caches
- * @param queryDefault the default options for tuning query caches
- */
- DefaultCacheHolder(ServerCacheFactory cacheFactory, ServerCacheOptions beanDefault, ServerCacheOptions queryDefault, CurrentTenantProvider tenantProvider) {
- this.cacheFactory = cacheFactory;
- this.beanDefault = beanDefault;
- this.queryDefault = queryDefault;
- this.tenantProvider = tenantProvider;
+ DefaultCacheHolder(CacheManagerOptions builder) {
+ this.cacheFactory = builder.getCacheFactory();
+ this.beanDefault = builder.getBeanDefault();
+ this.queryDefault = builder.getQueryDefault();
+ this.tenantProvider = builder.getCurrentTenantProvider();
+ this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate();
}
ServerCache getCache(Class> beanType, String cacheKey, ServerCacheType type) {
@@ -76,7 +70,7 @@ class DefaultCacheHolder {
collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key);
}
}
- return cacheFactory.createCache(type, key, tenantProvider, options);
+ return cacheFactory.createCache(new ServerCacheConfig(type, key, options, tenantProvider, queryCacheEntryValidate));
}
void clearAll() {
diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
index b8bee5cc4..33e85f34b 100644
--- a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
+++ b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java
@@ -2,10 +2,8 @@ package io.ebeaninternal.server.cache;
import io.ebean.BackgroundExecutor;
import io.ebean.cache.ServerCache;
-import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.cache.TenantAwareKey;
-import io.ebean.config.CurrentTenantProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,7 +13,6 @@ import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
@@ -65,47 +62,14 @@ public class DefaultServerCache implements ServerCache {
protected TenantAwareKey tenantAwareKey;
- /**
- * Construct using a ConcurrentHashMap and cache options.
- */
- public DefaultServerCache(String name, CurrentTenantProvider tenantProvider, ServerCacheOptions options) {
- this(name, new ConcurrentHashMap<>(), tenantProvider, options);
- }
-
- /**
- * Construct passing in name, map and base eviction controls as ServerCacheOptions.
- */
- public DefaultServerCache(String name, Map map, CurrentTenantProvider tenantProvider, ServerCacheOptions options) {
- this(name, map, tenantProvider, options.getMaxSize(), options.getMaxIdleSecs(), options.getMaxSecsToLive(), options.getTrimFrequency());
- }
-
- /**
- * Construct passing in name, map and base eviction controls.
- */
- public DefaultServerCache(String name, Map map, CurrentTenantProvider tenantProvider, int maxSize, int maxIdleSecs, int maxSecsToLive, int trimFrequency) {
- this.name = name;
- this.map = map;
- this.maxSize = maxSize;
- this.tenantAwareKey = new TenantAwareKey(tenantProvider);
- this.maxIdleSecs = maxIdleSecs;
- this.maxSecsToLive = maxSecsToLive;
- this.trimFrequency = determineTrim(maxIdleSecs, maxSecsToLive, trimFrequency);
- }
-
- /**
- * Determine a good trimFrequency as half of maxIdleSecs (or maxSecsToLive).
- */
- int determineTrim(int maxIdleSecs, int maxSecsToLive, int trimFrequency) {
- if (trimFrequency > 0) {
- return trimFrequency;
- }
- if (maxIdleSecs > 0) {
- return maxIdleSecs / 2 - 1;
- }
- if (maxSecsToLive > 0) {
- return maxSecsToLive / 2 - 1;
- }
- return 0;
+ public DefaultServerCache(DefaultServerCacheConfig config) {
+ this.name = config.getName();
+ this.map = config.getMap();
+ this.maxSize = config.getMaxSize();
+ this.tenantAwareKey = new TenantAwareKey(config.getTenantProvider());
+ this.maxIdleSecs = config.getMaxIdleSecs();
+ this.maxSecsToLive = config.getMaxSecsToLive();
+ this.trimFrequency = config.determineTrimFrequency();
}
public void periodicTrim(BackgroundExecutor executor) {
@@ -193,7 +157,7 @@ public class DefaultServerCache implements ServerCache {
/**
* Return the tenant aware key.
*/
- private Object key(Object id) {
+ protected Object key(Object id) {
return tenantAwareKey.key(id);
}
@@ -203,7 +167,7 @@ public class DefaultServerCache implements ServerCache {
@Override
public Object get(Object id) {
- CacheEntry entry = map.get(key(id));
+ CacheEntry entry = getCacheEntry(id);
if (entry == null) {
missCount.increment();
return null;
@@ -212,10 +176,24 @@ public class DefaultServerCache implements ServerCache {
// Important that hitCount.increment() MUST be low latency under concurrent
// use hence must use LongAdder or better here
hitCount.increment();
- return entry.getValue();
+ return unwrapEntry(entry);
}
}
+ /**
+ * Unwrap the cache entry - override for query cache to unwrap to the query result.
+ */
+ protected Object unwrapEntry(CacheEntry entry) {
+ return entry.getValue();
+ }
+
+ /**
+ * Get the cache entry - override for query cache to validate dependent tables.
+ */
+ protected CacheEntry getCacheEntry(Object id) {
+ return map.get(key(id));
+ }
+
@Override
public void putAll(Map keyValues) {
keyValues.forEach(this::put);
diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheConfig.java b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheConfig.java
new file mode 100644
index 000000000..44538beed
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheConfig.java
@@ -0,0 +1,80 @@
+package io.ebeaninternal.server.cache;
+
+import io.ebean.cache.QueryCacheEntryValidate;
+import io.ebean.cache.ServerCacheConfig;
+import io.ebean.cache.ServerCacheOptions;
+import io.ebean.config.CurrentTenantProvider;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class DefaultServerCacheConfig {
+
+ private final ServerCacheConfig config;
+
+ private int maxSize;
+ private int maxIdleSecs;
+ private int maxSecsToLive;
+ private int trimFrequency;
+
+ private Map map;
+
+ public DefaultServerCacheConfig(ServerCacheConfig config) {
+ this(config, new ConcurrentHashMap<>());
+ }
+
+ public DefaultServerCacheConfig(ServerCacheConfig config, Map map) {
+ this.config = config;
+ this.map = map;
+
+ ServerCacheOptions options = config.getCacheOptions();
+ this.maxIdleSecs = options.getMaxIdleSecs();
+ this.maxSecsToLive = options.getMaxSecsToLive();
+ this.trimFrequency = options.getTrimFrequency();
+ this.maxSize = options.getMaxSize();
+ }
+
+ public CurrentTenantProvider getTenantProvider() {
+ return config.getTenantProvider();
+ }
+
+ public QueryCacheEntryValidate getQueryCacheEntryValidate() {
+ return config.getQueryCacheEntryValidate();
+ }
+
+ public String getName() {
+ return config.getCacheKey();
+ }
+
+ public Map getMap() {
+ return map;
+ }
+
+ public int getMaxSize() {
+ return maxSize;
+ }
+
+ public int getMaxIdleSecs() {
+ return maxIdleSecs;
+ }
+
+ public int getMaxSecsToLive() {
+ return maxSecsToLive;
+ }
+
+ /**
+ * Determine a good trimFrequency as half of maxIdleSecs (or maxSecsToLive).
+ */
+ public int determineTrimFrequency() {
+ if (trimFrequency > 0) {
+ return trimFrequency;
+ }
+ if (maxIdleSecs > 0) {
+ return maxIdleSecs / 2 - 1;
+ }
+ if (maxSecsToLive > 0) {
+ return maxSecsToLive / 2 - 1;
+ }
+ return 0;
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheFactory.java b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheFactory.java
index 197052dcb..a519ea469 100644
--- a/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheFactory.java
+++ b/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheFactory.java
@@ -2,10 +2,8 @@ package io.ebeaninternal.server.cache;
import io.ebean.BackgroundExecutor;
import io.ebean.cache.ServerCache;
+import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheFactory;
-import io.ebean.cache.ServerCacheOptions;
-import io.ebean.cache.ServerCacheType;
-import io.ebean.config.CurrentTenantProvider;
/**
@@ -30,9 +28,15 @@ class DefaultServerCacheFactory implements ServerCacheFactory {
}
@Override
- public ServerCache createCache(ServerCacheType type, String cacheKey, CurrentTenantProvider tenantProvider, ServerCacheOptions cacheOptions) {
+ public ServerCache createCache(ServerCacheConfig config) {
- DefaultServerCache cache = new DefaultServerCache(cacheKey, tenantProvider, cacheOptions);
+ DefaultServerCache cache;
+ if (config.isQueryCache()) {
+ // use a server cache aware of extra validation and QueryCacheEntry
+ cache = new DefaultServerQueryCache(new DefaultServerCacheConfig(config));
+ } else {
+ cache = new DefaultServerCache(new DefaultServerCacheConfig(config));
+ }
if (executor != null) {
cache.periodicTrim(executor);
}
diff --git a/src/main/java/io/ebeaninternal/server/cache/DefaultServerQueryCache.java b/src/main/java/io/ebeaninternal/server/cache/DefaultServerQueryCache.java
new file mode 100644
index 000000000..85f4b22ca
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/cache/DefaultServerQueryCache.java
@@ -0,0 +1,41 @@
+package io.ebeaninternal.server.cache;
+
+import io.ebean.cache.QueryCacheEntry;
+import io.ebean.cache.QueryCacheEntryValidate;
+
+/**
+ * Server cache for query caching.
+ *
+ * Entries in this cache contain QueryCacheEntry and we need to additionally
+ * validate the entries when hit for changes to dependent tables.
+ *
+ */
+public class DefaultServerQueryCache extends DefaultServerCache {
+
+ private final QueryCacheEntryValidate queryCacheEntryValidate;
+
+ public DefaultServerQueryCache(DefaultServerCacheConfig config) {
+ super(config);
+ this.queryCacheEntryValidate = config.getQueryCacheEntryValidate();
+ }
+
+ protected Object unwrapEntry(CacheEntry entry) {
+ return ((QueryCacheEntry) entry.getValue()).getValue();
+ }
+
+ @Override
+ protected CacheEntry getCacheEntry(Object id) {
+ Object key = key(id);
+ CacheEntry entry = map.get(key);
+ if (entry == null) {
+ return null;
+ }
+ QueryCacheEntry value = (QueryCacheEntry) entry.getValue();
+ if (!queryCacheEntryValidate.isValid(value)) {
+ map.remove(key);
+ removeCount.increment();
+ return null;
+ }
+ return entry;
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/core/CacheOptions.java b/src/main/java/io/ebeaninternal/server/core/CacheOptions.java
index 5cb7741a2..24d441828 100644
--- a/src/main/java/io/ebeaninternal/server/core/CacheOptions.java
+++ b/src/main/java/io/ebeaninternal/server/core/CacheOptions.java
@@ -12,6 +12,9 @@ public class CacheOptions {
*/
public static final CacheOptions NO_CACHING = new CacheOptions();
+ public static final CacheOptions INVALIDATE_QUERY_CACHE = new CacheOptions(true);
+
+ private final boolean invalidateQueryCache;
private final boolean enableBeanCache;
private final boolean enableQueryCache;
private final boolean readOnly;
@@ -21,6 +24,18 @@ public class CacheOptions {
* Construct for no caching.
*/
private CacheOptions() {
+ invalidateQueryCache = false;
+ enableBeanCache = false;
+ enableQueryCache = false;
+ readOnly = false;
+ naturalKey = null;
+ }
+
+ /**
+ * Construct for invalidateQueryCache.
+ */
+ private CacheOptions(boolean invalidateQueryCache) {
+ this.invalidateQueryCache = invalidateQueryCache;
enableBeanCache = false;
enableQueryCache = false;
readOnly = false;
@@ -31,12 +46,21 @@ public class CacheOptions {
* Construct with cache annotation.
*/
public CacheOptions(Cache cache, String[] naturalKey) {
+ invalidateQueryCache = false;
enableBeanCache = cache.enableBeanCache();
enableQueryCache = cache.enableQueryCache();
readOnly = cache.readOnly();
this.naturalKey = naturalKey;
}
+ /**
+ * Return true if this is InvalidateQueryCache. A Bean that itself isn't L2
+ * cached but invalidates query cache entries that join to it.
+ */
+ public boolean isInvalidateQueryCache() {
+ return invalidateQueryCache;
+ }
+
/**
* Return true if bean caching is enabled.
*/
diff --git a/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java b/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
index a8cf20c08..0428a8893 100644
--- a/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
+++ b/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java
@@ -138,11 +138,10 @@ public class DefaultContainer implements SpiContainer {
// executor and l2 caching service setup early (used during server construction)
SpiBackgroundExecutor executor = createBackgroundExecutor(serverConfig);
- SpiCacheManager cacheManager = getCacheManager(online, serverConfig, executor);
- InternalConfiguration c = new InternalConfiguration(clusterManager, cacheManager, executor, serverConfig, bootupClasses);
+ InternalConfiguration c = new InternalConfiguration(online, clusterManager, executor, serverConfig, bootupClasses);
- DefaultServer server = new DefaultServer(c, c.cache());
+ DefaultServer server = new DefaultServer(c, c.cacheManager());
// generate and run DDL if required
// if there are any other tasks requiring action in their plugins, do them as well
@@ -165,52 +164,6 @@ public class DefaultContainer implements SpiContainer {
}
}
- /**
- * Create and return the CacheManager.
- */
- private SpiCacheManager getCacheManager(boolean online, ServerConfig serverConfig, BackgroundExecutor executor) {
-
- if (!online || serverConfig.isDisableL2Cache()) {
- // use local only L2 cache implementation as placeholder
- return new DefaultServerCacheManager();
- }
-
- // reasonable default settings are for a cache per bean type
- ServerCacheOptions beanOptions = new ServerCacheOptions();
- beanOptions.setMaxSize(serverConfig.getCacheMaxSize());
- beanOptions.setMaxIdleSecs(serverConfig.getCacheMaxIdleTime());
- beanOptions.setMaxSecsToLive(serverConfig.getCacheMaxTimeToLive());
-
- // reasonable default settings for the query cache per bean type
- ServerCacheOptions queryOptions = new ServerCacheOptions();
- queryOptions.setMaxSize(serverConfig.getQueryCacheMaxSize());
- queryOptions.setMaxIdleSecs(serverConfig.getQueryCacheMaxIdleTime());
- queryOptions.setMaxSecsToLive(serverConfig.getQueryCacheMaxTimeToLive());
-
- boolean localL2Caching = false;
- ServerCachePlugin plugin = serverConfig.getServerCachePlugin();
- if (plugin == null) {
- ServiceLoader cacheFactories = ServiceLoader.load(ServerCachePlugin.class);
- Iterator iterator = cacheFactories.iterator();
- if (iterator.hasNext()) {
- // use the cacheFactory (via classpath service loader)
- plugin = iterator.next();
- logger.debug("using ServerCacheFactory {}", plugin.getClass());
- } else {
- // use the built in default l2 caching which is local cache based
- localL2Caching = true;
- plugin = new DefaultServerCachePlugin();
- }
- }
-
- ServerCacheFactory factory = plugin.create(serverConfig, executor);
-
- CacheManagerOptions builder = new CacheManagerOptions(clusterManager, serverConfig, localL2Caching)
- .with(beanOptions, queryOptions)
- .with(factory);
- return new DefaultServerCacheManager(builder);
- }
-
/**
* Get the entities, scalarTypes, Listeners etc combining the class registered
* ones with the already created instances.
diff --git a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
index 4ea1988a1..8d201d9c9 100644
--- a/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
+++ b/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java
@@ -3,7 +3,10 @@ package io.ebeaninternal.server.core;
import com.fasterxml.jackson.core.JsonFactory;
import io.ebean.ExpressionFactory;
import io.ebean.annotation.Platform;
+import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheManager;
+import io.ebean.cache.ServerCacheOptions;
+import io.ebean.cache.ServerCachePlugin;
import io.ebean.config.ExternalTransactionManager;
import io.ebean.config.ProfilingConfig;
import io.ebean.config.ServerConfig;
@@ -27,7 +30,10 @@ import io.ebeaninternal.api.SpiProfileHandler;
import io.ebeaninternal.dbmigration.DbOffline;
import io.ebeaninternal.server.autotune.AutoTuneService;
import io.ebeaninternal.server.autotune.service.AutoTuneServiceFactory;
+import io.ebeaninternal.server.cache.CacheManagerOptions;
import io.ebeaninternal.server.cache.DefaultCacheAdapter;
+import io.ebeaninternal.server.cache.DefaultServerCacheManager;
+import io.ebeaninternal.server.cache.DefaultServerCachePlugin;
import io.ebeaninternal.server.cache.SpiCacheManager;
import io.ebeaninternal.server.changelog.DefaultChangeLogListener;
import io.ebeaninternal.server.changelog.DefaultChangeLogPrepare;
@@ -69,6 +75,7 @@ import io.ebeaninternal.server.transaction.ExplicitTransactionManager;
import io.ebeaninternal.server.transaction.ExternalTransactionScopeManager;
import io.ebeaninternal.server.transaction.JtaTransactionManager;
import io.ebeaninternal.server.transaction.NoopProfileHandler;
+import io.ebeaninternal.server.transaction.TableModState;
import io.ebeaninternal.server.transaction.TransactionManager;
import io.ebeaninternal.server.transaction.TransactionManagerOptions;
import io.ebeaninternal.server.transaction.TransactionScopeManager;
@@ -84,6 +91,7 @@ import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
@@ -96,6 +104,10 @@ public class InternalConfiguration {
private static final Logger logger = LoggerFactory.getLogger(InternalConfiguration.class);
+ private final TableModState tableModState = new TableModState();
+
+ private final boolean online;
+
private final ServerConfig serverConfig;
private final BootupClasses bootupClasses;
@@ -124,6 +136,10 @@ public class InternalConfiguration {
private final SpiCacheManager cacheManager;
+ private final ServerCachePlugin serverCachePlugin;
+
+ private boolean localL2Caching;
+
private final ExpressionFactory expressionFactory;
private final SpiBackgroundExecutor backgroundExecutor;
@@ -141,17 +157,16 @@ public class InternalConfiguration {
private final SpiLogManager logManager;
- public InternalConfiguration(ClusterManager clusterManager,
- SpiCacheManager cacheManager, SpiBackgroundExecutor backgroundExecutor,
+ public InternalConfiguration(boolean online, ClusterManager clusterManager, SpiBackgroundExecutor backgroundExecutor,
ServerConfig serverConfig, BootupClasses bootupClasses) {
+ this.online = online;
this.serverConfig = serverConfig;
this.logManager = initLogManager();
this.docStoreFactory = initDocStoreFactory(serverConfig.service(DocStoreFactory.class));
this.jsonFactory = serverConfig.getJsonFactory();
this.clusterManager = clusterManager;
this.backgroundExecutor = backgroundExecutor;
- this.cacheManager = cacheManager;
this.bootupClasses = bootupClasses;
this.databasePlatform = serverConfig.getDatabasePlatform();
@@ -164,6 +179,9 @@ public class InternalConfiguration {
this.deployCreateProperties = new DeployCreateProperties(typeManager);
this.deployUtil = new DeployUtil(typeManager, serverConfig);
+ this.serverCachePlugin = initServerCachePlugin();
+ this.cacheManager = initCacheManager();
+
InternalConfigXmlRead xmlRead = new InternalConfigXmlRead(serverConfig);
this.dtoBeanManager = new DtoBeanManager(typeManager, xmlRead.readDtoMapping());
@@ -400,7 +418,7 @@ public class InternalConfiguration {
TransactionManagerOptions options =
new TransactionManagerOptions(notifyL2CacheInForeground, serverConfig, scopeManager, clusterManager, backgroundExecutor,
- indexUpdateProcessor, beanDescriptorManager, dataSource(), profileHandler(), logManager);
+ indexUpdateProcessor, beanDescriptorManager, dataSource(), profileHandler(), logManager, tableModState);
if (serverConfig.isExplicitTransactionBeginMode()) {
return new ExplicitTransactionManager(options);
@@ -493,7 +511,7 @@ public class InternalConfiguration {
return dataTimeZone;
}
- public ServerCacheManager cache() {
+ public ServerCacheManager cacheManager() {
return new DefaultCacheAdapter(cacheManager);
}
@@ -537,4 +555,54 @@ public class InternalConfiguration {
public SpiLogManager getLogManager() {
return logManager;
}
+
+ private ServerCachePlugin initServerCachePlugin() {
+
+ ServerCachePlugin plugin = serverConfig.getServerCachePlugin();
+ if (plugin == null) {
+ ServiceLoader cacheFactories = ServiceLoader.load(ServerCachePlugin.class);
+ Iterator iterator = cacheFactories.iterator();
+ if (iterator.hasNext()) {
+ // use the cacheFactory (via classpath service loader)
+ plugin = iterator.next();
+ logger.debug("using ServerCacheFactory {}", serverCachePlugin.getClass());
+ } else {
+ // use the built in default l2 caching which is local cache based
+ localL2Caching = true;
+ plugin = new DefaultServerCachePlugin();
+ }
+ }
+ return plugin;
+ }
+
+ /**
+ * Create and return the CacheManager.
+ */
+ private SpiCacheManager initCacheManager() {
+
+ if (!online || serverConfig.isDisableL2Cache()) {
+ // use local only L2 cache implementation as placeholder
+ return new DefaultServerCacheManager();
+ }
+
+ ServerCacheFactory factory = serverCachePlugin.create(serverConfig, backgroundExecutor);
+
+ // reasonable default settings are for a cache per bean type
+ ServerCacheOptions beanOptions = new ServerCacheOptions();
+ beanOptions.setMaxSize(serverConfig.getCacheMaxSize());
+ beanOptions.setMaxIdleSecs(serverConfig.getCacheMaxIdleTime());
+ beanOptions.setMaxSecsToLive(serverConfig.getCacheMaxTimeToLive());
+
+ // reasonable default settings for the query cache per bean type
+ ServerCacheOptions queryOptions = new ServerCacheOptions();
+ queryOptions.setMaxSize(serverConfig.getQueryCacheMaxSize());
+ queryOptions.setMaxIdleSecs(serverConfig.getQueryCacheMaxIdleTime());
+ queryOptions.setMaxSecsToLive(serverConfig.getQueryCacheMaxTimeToLive());
+
+ CacheManagerOptions builder = new CacheManagerOptions(clusterManager, serverConfig, localL2Caching)
+ .with(beanOptions, queryOptions)
+ .with(factory, tableModState);
+
+ return new DefaultServerCacheManager(builder);
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
index 799c5b9d2..264af7bc8 100644
--- a/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
+++ b/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java
@@ -8,6 +8,7 @@ import io.ebean.Version;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
+import io.ebean.cache.QueryCacheEntry;
import io.ebean.common.BeanList;
import io.ebean.common.CopyOnFirstWriteList;
import io.ebean.event.BeanFindController;
@@ -83,6 +84,8 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery
private boolean inlineCountDistinct;
+ private Set dependentTables;
+
/**
* Create the InternalQueryRequest.
*/
@@ -207,7 +210,7 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery
/**
* Prepare the query and calculate the query plan key.
*/
- public void prepareQuery() {
+ void prepareQuery() {
beanDescriptor.prepareQuery(query);
adapterPreQuery();
this.secondaryQueries = query.convertJoins();
@@ -680,8 +683,10 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery
}
}
- public void putToQueryCache(Object queryResult) {
- beanDescriptor.queryCachePut(cacheKey, queryResult);
+ public void putToQueryCache(Object result) {
+ // use transaction start where as query statement start would be better at READ_COMMITTED
+ long asOfTimestamp = transaction.getStartMillis();
+ beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, asOfTimestamp));
}
/**
@@ -752,4 +757,13 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery
public boolean isInlineCountDistinct() {
return inlineCountDistinct;
}
+
+ public void addDependentTables(Set tables) {
+ if (tables != null && !tables.isEmpty()) {
+ if (dependentTables == null) {
+ dependentTables = new LinkedHashSet<>();
+ }
+ dependentTables.addAll(tables);
+ }
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java
index ba0ecc52d..60b5ed482 100644
--- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java
+++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java
@@ -11,6 +11,7 @@ import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.PersistenceContext;
+import io.ebean.cache.QueryCacheEntry;
import io.ebean.config.EncryptKey;
import io.ebean.config.ServerConfig;
import io.ebean.config.dbplatform.IdType;
@@ -1361,8 +1362,8 @@ public class BeanDescriptor implements BeanType, STreeType {
/**
* Put a query result into the query cache.
*/
- public void queryCachePut(Object id, Object queryResult) {
- cacheHelp.queryCachePut(id, queryResult);
+ public void queryCachePut(Object id, QueryCacheEntry entry) {
+ cacheHelp.queryCachePut(id, entry);
}
/**
diff --git a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java
index 1d11da087..07f326e6c 100644
--- a/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java
+++ b/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java
@@ -4,6 +4,7 @@ import io.ebean.bean.BeanCollection;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.PersistenceContext;
+import io.ebean.cache.QueryCacheEntry;
import io.ebean.cache.ServerCache;
import io.ebeaninternal.api.BeanCacheResult;
import io.ebeaninternal.api.TransactionEventTable.TableIUD;
@@ -54,6 +55,7 @@ final class BeanDescriptorCacheHelp {
* Flag indicating this bean has no relationships.
*/
private final boolean cacheSharableBeans;
+ private final boolean invalidateQueryCache;
private final Class> beanType;
@@ -84,6 +86,7 @@ final class BeanDescriptorCacheHelp {
this.cacheName = beanType.getSimpleName();
this.cacheManager = cacheManager;
this.cacheOptions = cacheOptions;
+ this.invalidateQueryCache = cacheOptions.isInvalidateQueryCache();
this.cacheSharableBeans = cacheSharableBeans;
this.propertiesOneImported = propertiesOneImported;
this.naturalKey = cacheOptions.getNaturalKey();
@@ -111,7 +114,7 @@ final class BeanDescriptorCacheHelp {
* Derive the cache notify flags.
*/
void deriveNotifyFlags() {
- cacheNotifyOnAll = (beanCache != null || queryCache != null);
+ cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null);
cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes();
if (logger.isDebugEnabled()) {
@@ -225,14 +228,14 @@ final class BeanDescriptorCacheHelp {
/**
* Put a query result into the query cache.
*/
- void queryCachePut(Object id, Object queryResult) {
+ void queryCachePut(Object id, QueryCacheEntry entry) {
if (queryCache == null) {
throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)");
}
if (queryLog.isDebugEnabled()) {
queryLog.debug(" PUT {}({})", cacheName, id);
}
- queryCache.put(id, queryResult);
+ queryCache.put(id, entry);
}
@@ -723,30 +726,42 @@ final class BeanDescriptorCacheHelp {
* Add appropriate cache changes to support delete by id.
*/
void handleDelete(Object id, CacheChangeSet changeSet) {
- if (beanCache != null) {
- changeSet.addBeanRemove(desc, id);
+ if (invalidateQueryCache) {
+ changeSet.addInvalidate(desc);
+ } else {
+ if (beanCache != null) {
+ changeSet.addBeanRemove(desc, id);
+ }
+ cacheDeleteImported(true, null, changeSet);
}
- cacheDeleteImported(true, null, changeSet);
}
/**
* Add appropriate cache changes to support delete bean.
*/
void handleDelete(Object id, PersistRequestBean deleteRequest, CacheChangeSet changeSet) {
- queryCacheClear(changeSet);
- if (beanCache != null) {
- changeSet.addBeanRemove(desc, id);
+ if (invalidateQueryCache) {
+ changeSet.addInvalidate(desc);
+ } else {
+ queryCacheClear(changeSet);
+ if (beanCache != null) {
+ changeSet.addBeanRemove(desc, id);
+ }
+ cacheDeleteImported(true, deleteRequest.getEntityBean(), changeSet);
}
- cacheDeleteImported(true, deleteRequest.getEntityBean(), changeSet);
}
/**
* Add appropriate cache changes to support insert.
*/
void handleInsert(PersistRequestBean insertRequest, CacheChangeSet changeSet) {
- queryCacheClear(changeSet);
- cacheDeleteImported(false, insertRequest.getEntityBean(), changeSet);
- changeSet.addBeanInsert(desc.getBaseTable());
+ if (invalidateQueryCache) {
+ changeSet.addInvalidate(desc);
+ } else {
+ queryCacheClear(changeSet);
+ cacheDeleteImported(false, insertRequest.getEntityBean(), changeSet);
+ changeSet.addBeanInsert(desc.getBaseTable());
+ }
}
private void cacheDeleteImported(boolean clear, EntityBean entityBean, CacheChangeSet changeSet) {
@@ -759,28 +774,30 @@ final class BeanDescriptorCacheHelp {
* Add appropriate changes to support update.
*/
void handleUpdate(Object id, PersistRequestBean updateRequest, CacheChangeSet changeSet) {
+ if (invalidateQueryCache) {
+ changeSet.addInvalidate(desc);
- queryCacheClear(changeSet);
+ } else {
+ queryCacheClear(changeSet);
+ if (beanCache == null) {
+ // query caching only
+ return;
+ }
- if (beanCache == null) {
- // query caching only
- return;
- }
-
- List> manyCollections = updateRequest.getUpdatedManyCollections();
- if (manyCollections != null) {
- for (BeanPropertyAssocMany> many : manyCollections) {
- if (!many.isElementCollection()) {
- Object details = many.getValue(updateRequest.getEntityBean());
- CachedManyIds entry = createManyIds(many, details);
- if (entry != null) {
- changeSet.addManyPut(desc, many.getName(), id, entry);
+ List> manyCollections = updateRequest.getUpdatedManyCollections();
+ if (manyCollections != null) {
+ for (BeanPropertyAssocMany> many : manyCollections) {
+ if (!many.isElementCollection()) {
+ Object details = many.getValue(updateRequest.getEntityBean());
+ CachedManyIds entry = createManyIds(many, details);
+ if (entry != null) {
+ changeSet.addManyPut(desc, many.getName(), id, entry);
+ }
}
}
}
+ updateRequest.addBeanUpdate(changeSet);
}
-
- updateRequest.addBeanUpdate(changeSet);
}
/**
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 199b7a90b..24eca35eb 100644
--- a/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java
+++ b/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java
@@ -440,6 +440,13 @@ public class DeployBeanDescriptor {
this.inheritInfo = inheritInfo;
}
+ /**
+ * Set that this type invalidates query caches.
+ */
+ public void setInvalidateQueryCache() {
+ this.cacheOptions = CacheOptions.INVALIDATE_QUERY_CACHE;
+ }
+
/**
* Enable L2 bean and query caching based on Cache annotation.
*/
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 208b0bdf3..f28f5e303 100644
--- a/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java
+++ b/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationClass.java
@@ -7,6 +7,7 @@ import io.ebean.annotation.Draftable;
import io.ebean.annotation.DraftableElement;
import io.ebean.annotation.History;
import io.ebean.annotation.Index;
+import io.ebean.annotation.InvalidateQueryCache;
import io.ebean.annotation.ReadAudit;
import io.ebean.annotation.UpdateMode;
import io.ebean.annotation.View;
@@ -181,9 +182,16 @@ public class AnnotationClass extends AnnotationParser {
descriptor.setUpdateChangesOnly(updateMode.updateChangesOnly());
}
- Cache cache = AnnotationUtil.findAnnotationRecursive(cls, Cache.class);
- if (cache != null && !disableL2Cache) {
- descriptor.setCache(cache);
+ if (!disableL2Cache) {
+ Cache cache = AnnotationUtil.findAnnotationRecursive(cls, Cache.class);
+ if (cache != null) {
+ descriptor.setCache(cache);
+ } else {
+ InvalidateQueryCache invalidateQueryCache = AnnotationUtil.findAnnotationRecursive(cls, InvalidateQueryCache.class);
+ if (invalidateQueryCache != null) {
+ descriptor.setInvalidateQueryCache();
+ }
+ }
}
Set namedQueries = AnnotationUtil.findAnnotationsRecursive(cls, NamedQuery.class);
diff --git a/src/main/java/io/ebeaninternal/server/query/CQuery.java b/src/main/java/io/ebeaninternal/server/query/CQuery.java
index 9a40afaaf..cdc0a7138 100644
--- a/src/main/java/io/ebeaninternal/server/query/CQuery.java
+++ b/src/main/java/io/ebeaninternal/server/query/CQuery.java
@@ -38,6 +38,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Set;
/**
* An object that represents a SqlSelect statement.
@@ -811,4 +812,8 @@ public class CQuery implements DbReadContext, CancelableQuery, SpiProfileTran
PreparedStatement getPstmt() {
return pstmt;
}
+
+ public Set getDependentTables() {
+ return queryPlan.getDependentTables();
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java
index a38a9e61a..05e64658a 100644
--- a/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java
+++ b/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java
@@ -25,6 +25,8 @@ import org.slf4j.LoggerFactory;
import javax.persistence.PersistenceException;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -111,6 +113,15 @@ public class CQueryEngine {
if (request.isLogSummary()) {
request.getTransaction().logSummary(rcQuery.getSummary());
}
+ if (request.isQueryCachePut() && !list.isEmpty()) {
+ request.addDependentTables(rcQuery.getDependentTables());
+
+ list = Collections.unmodifiableList(list);
+ request.putToQueryCache(list);
+ if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) {
+ list = new ArrayList<>(list);
+ }
+ }
return list;
} catch (SQLException e) {
@@ -173,6 +184,11 @@ public class CQueryEngine {
request.getTransaction().end();
}
+ if (request.isQueryCachePut()) {
+ request.addDependentTables(rcQuery.getDependentTables());
+ request.putToQueryCache(count);
+ }
+
return count;
} catch (SQLException e) {
@@ -385,6 +401,9 @@ public class CQueryEngine {
}
request.executeSecondaryQueries(false);
+ if (request.isQueryCachePut()) {
+ request.addDependentTables(cquery.getDependentTables());
+ }
return beanCollection;
diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java b/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java
index b5d2d05b1..3b8b8ef01 100644
--- a/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java
+++ b/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java
@@ -17,6 +17,7 @@ import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
* Base compiled query request for single attribute queries.
@@ -188,4 +189,8 @@ class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent {
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.getProfileId(), rowCount, query.getProfileId());
}
+
+ Set getDependentTables() {
+ return queryPlan.getDependentTables();
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java b/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java
index a0d9a46b1..7b4b27e18 100644
--- a/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java
+++ b/src/main/java/io/ebeaninternal/server/query/CQueryPlan.java
@@ -24,6 +24,8 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.Collections;
+import java.util.Set;
/**
* Represents a query for a given SQL statement.
@@ -88,6 +90,8 @@ public class CQueryPlan {
*/
private volatile String auditQueryHash;
+ private final Set dependentTables;
+
/**
* Create a query plan based on a OrmQueryRequest.
*/
@@ -110,6 +114,7 @@ public class CQueryPlan {
this.logWhereSql = logWhereSql;
this.encryptedProps = sqlTree.getEncryptedProps();
this.stats = new CQueryPlanStats(this, server.isCollectQueryOrigins());
+ this.dependentTables = sqlTree.dependentTables();
}
/**
@@ -134,6 +139,7 @@ public class CQueryPlan {
this.logWhereSql = logWhereSql;
this.encryptedProps = sqlTree.getEncryptedProps();
this.stats = new CQueryPlanStats(this, server.isCollectQueryOrigins());
+ this.dependentTables = (rawSql) ? Collections.emptySet() : sqlTree.dependentTables();
}
private String location() {
@@ -154,6 +160,10 @@ public class CQueryPlan {
return beanType;
}
+ public Set getDependentTables() {
+ return dependentTables;
+ }
+
public ProfileLocation getProfileLocation() {
return profileLocation;
}
diff --git a/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java b/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java
index 5ddfe7b02..e0f9c1dbb 100644
--- a/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java
+++ b/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java
@@ -12,6 +12,7 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.Set;
/**
* Executes the select row count query.
@@ -154,4 +155,8 @@ class CQueryRowCount implements SpiProfileTransactionEvent {
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.getProfileId(), rowCount, query.getProfileId());
}
+
+ Set getDependentTables() {
+ return queryPlan.getDependentTables();
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java
index c6c042c92..d72a2f688 100644
--- a/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java
+++ b/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java
@@ -88,41 +88,20 @@ public class DefaultOrmQueryEngine implements OrmQueryEngine {
public int findCount(OrmQueryRequest request) {
flushJdbcBatchOnQuery(request);
- int result = queryEngine.findCount(request);
- if (request.getQuery().getUseQueryCache().isPut()) {
- request.putToQueryCache(result);
- }
- return result;
+ return queryEngine.findCount(request);
}
@Override
public List findIds(OrmQueryRequest> request) {
flushJdbcBatchOnQuery(request);
- List result = queryEngine.findIds(request);
- if (request.getQuery().getUseQueryCache().isPut()) {
- result = Collections.unmodifiableList(result);
- request.putToQueryCache(result);
- if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) {
- result = new ArrayList<>(result);
- }
- }
- return result;
+ return queryEngine.findIds(request);
}
@Override
public List findSingleAttributeList(OrmQueryRequest> request) {
flushJdbcBatchOnQuery(request);
- List result = queryEngine.findSingleAttributeList(request);
- if (!result.isEmpty() && request.getQuery().getUseQueryCache().isPut()) {
- // load the query result into the query cache
- result = Collections.unmodifiableList(result);
- request.putToQueryCache(result);
- if (Boolean.FALSE.equals(request.getQuery().isReadOnly())) {
- result = new ArrayList<>(result);
- }
- }
- return result;
+ return queryEngine.findSingleAttributeList(request);
}
@Override
diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTree.java b/src/main/java/io/ebeaninternal/server/query/SqlTree.java
index b6ee3c980..91e71fb0d 100644
--- a/src/main/java/io/ebeaninternal/server/query/SqlTree.java
+++ b/src/main/java/io/ebeaninternal/server/query/SqlTree.java
@@ -3,6 +3,7 @@ package io.ebeaninternal.server.query;
import io.ebeaninternal.api.SpiQuery;
import java.util.ArrayList;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -163,4 +164,13 @@ class SqlTree {
boolean isSingleProperty() {
return rootNode.isSingleProperty();
}
+
+ /**
+ * Return the tables that are joined in this query.
+ */
+ Set dependentTables() {
+ Set tables = new LinkedHashSet<>();
+ rootNode.dependentTables(tables);
+ return tables;
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java
index c503581d8..39e0bff28 100644
--- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java
+++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNode.java
@@ -9,6 +9,7 @@ import io.ebeaninternal.server.type.ScalarType;
import java.sql.SQLException;
import java.util.List;
+import java.util.Set;
interface SqlTreeNode {
@@ -87,4 +88,9 @@ interface SqlTreeNode {
* Return true if the query is known to only have a single property selected.
*/
boolean isSingleProperty();
+
+ /**
+ * Add dependent tables to the given set.
+ */
+ void dependentTables(Set tables);
}
diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java
index 339b08a95..9bafd1317 100644
--- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java
+++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeBean.java
@@ -21,6 +21,7 @@ import java.sql.Timestamp;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Normal bean included in the query.
@@ -578,6 +579,14 @@ class SqlTreeNodeBean implements SqlTreeNode {
}
}
+ @Override
+ public void dependentTables(Set tables) {
+ tables.add(nodeBeanProp.target().getBaseTable(temporalMode));
+ for (SqlTreeNode child : children) {
+ child.dependentTables(tables);
+ }
+ }
+
/**
* Join to base table for this node. This includes a join to the intersection
* table if this is a ManyToMany node.
diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java
index b517965e4..6dfd1fd7b 100644
--- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java
+++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeExtraJoin.java
@@ -11,6 +11,7 @@ import io.ebeaninternal.server.type.ScalarType;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
* The purpose is to add an extra join to the query.
@@ -100,6 +101,16 @@ class SqlTreeNodeExtraJoin implements SqlTreeNode {
children.add(child);
}
+ @Override
+ public void dependentTables(Set tables) {
+ tables.add(assocBeanProperty.target().getBaseTable(SpiQuery.TemporalMode.CURRENT));
+ if (children != null) {
+ for (SqlTreeNode child : children) {
+ child.dependentTables(tables);
+ }
+ }
+ }
+
@Override
public void appendFrom(DbSqlContext ctx, SqlJoinType joinType) {
diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java
index edeff9fce..ec8f6aa47 100644
--- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java
+++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyRoot.java
@@ -7,6 +7,7 @@ import io.ebeaninternal.server.deploy.DbSqlContext;
import java.sql.SQLException;
import java.util.List;
+import java.util.Set;
final class SqlTreeNodeManyRoot extends SqlTreeNodeBean {
diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java
index c31ea619c..a5cd0e232 100644
--- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java
+++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeManyWhereJoin.java
@@ -10,6 +10,7 @@ import io.ebeaninternal.server.deploy.TableJoin;
import io.ebeaninternal.server.type.ScalarType;
import java.util.List;
+import java.util.Set;
/**
* Join to Many (or child of a many) to support where clause predicates on many properties.
@@ -109,6 +110,11 @@ class SqlTreeNodeManyWhereJoin implements SqlTreeNode {
}
}
+ @Override
+ public void dependentTables(Set tables) {
+ tables.add(nodeBeanProp.target().getBaseTable(SpiQuery.TemporalMode.CURRENT));
+ }
+
@Override
public void buildRawSqlSelectChain(List selectChain) {
// nothing to add
diff --git a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeRoot.java b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeRoot.java
index 74be7c415..2a69a0c16 100644
--- a/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeRoot.java
+++ b/src/main/java/io/ebeaninternal/server/query/SqlTreeNodeRoot.java
@@ -5,6 +5,7 @@ import io.ebeaninternal.server.deploy.DbSqlContext;
import io.ebeaninternal.server.deploy.TableJoin;
import java.util.List;
+import java.util.Set;
/**
* Represents the root node of the Sql Tree.
@@ -77,4 +78,10 @@ final class SqlTreeNodeRoot extends SqlTreeNodeBean {
return joinType;
}
+ @Override
+ public void dependentTables(Set tables) {
+ for (SqlTreeNode child : children) {
+ child.dependentTables(tables);
+ }
+ }
}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
index 066a7504e..1ccc44dbd 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java
@@ -67,6 +67,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
private Map userObjects;
private long startNanos;
+ private long startMillis;
/**
* Create without a tenantId.
@@ -79,6 +80,7 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
this.connection = connection;
this.persistenceContext = new DefaultPersistenceContext();
this.startNanos = System.nanoTime();
+ this.startMillis = manager.clockNowEpoch();
}
/**
@@ -89,6 +91,12 @@ class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEventCode
this.tenantId = tenantId;
}
+ @Override
+ public long getStartMillis() {
+ // not used on read only transaction
+ return startMillis;
+ }
+
@Override
public void setLabel(String label) {
// do nothing
diff --git a/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
index 4f6ec5bd1..da8e53781 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java
@@ -187,6 +187,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
protected ProfileLocation profileLocation;
protected final long startNanos;
+ private final long startMillis;
/**
* Create a new JdbcTransaction.
@@ -203,6 +204,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
this.startNanos = System.nanoTime();
if (manager == null) {
+ this.startMillis = System.currentTimeMillis();
this.logSql = false;
this.logSummary = false;
this.skipCacheAfterWrite = true;
@@ -210,6 +212,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
this.batchOnCascadeMode = PersistBatch.NONE;
this.onQueryOnly = OnQueryOnly.ROLLBACK;
} else {
+ this.startMillis = manager.clockNowEpoch();
this.logSql = manager.isLogSql();
this.logSummary = manager.isLogSummary();
this.skipCacheAfterWrite = manager.isSkipCacheAfterWrite();
@@ -235,6 +238,11 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
return label;
}
+ @Override
+ public long getStartMillis() {
+ return startMillis;
+ }
+
@Override
public long profileOffset() {
return (profileStream == null) ? 0 : profileStream.offset();
@@ -853,7 +861,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
public TransactionEvent getEvent() {
queryOnly = false;
if (event == null) {
- event = new TransactionEvent();
+ event = new TransactionEvent(startMillis);
}
return event;
}
@@ -1050,7 +1058,7 @@ public class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
// the event has been sent to the transaction manager
// for postCommit processing (l2 cache updates etc)
// start a new transaction event
- event = new TransactionEvent();
+ event = new TransactionEvent(startMillis);
} catch (Exception e) {
doRollback(e);
diff --git a/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java b/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java
index 62ace0ede..bd2b20c35 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java
@@ -47,6 +47,12 @@ class NoTransaction implements SpiTransaction {
}
+ @Override
+ public long getStartMillis() {
+ // not used
+ return System.currentTimeMillis();
+ }
+
@Override
public boolean isActive() {
// always false
diff --git a/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java b/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java
index ac3a57560..b715d2ee3 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/PostCommitProcessing.java
@@ -14,6 +14,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
+import java.util.Set;
/**
* Performs post commit processing using a background thread.
@@ -88,10 +89,10 @@ public final class PostCommitProcessing {
processTableEvents(event.getEventTables());
if (manager.notifyL2CacheInForeground) {
// process l2 cache changes in foreground
- processCacheChanges(event.buildCacheChanges(manager.viewInvalidation));
+ processCacheChanges(event.buildCacheChanges());
} else {
// collect l2 cache changes for delayed background processing
- cacheChanges = event.buildCacheChanges(manager.viewInvalidation);
+ cacheChanges = event.buildCacheChanges();
}
}
@@ -165,7 +166,12 @@ public final class PostCommitProcessing {
*/
private void processCacheChanges(CacheChangeSet cacheChanges) {
if (cacheChanges != null) {
- manager.processViewInvalidation(cacheChanges.apply());
+ Set touched = cacheChanges.touchedTables();
+ if (touched != null && !touched.isEmpty()) {
+ manager.processTouchedTables(touched, cacheChanges.modificationTimestamp());
+ // TODO: Propagate touched tables to other cluster members
+ }
+ cacheChanges.apply();
}
}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TableModState.java b/src/main/java/io/ebeaninternal/server/transaction/TableModState.java
new file mode 100644
index 000000000..d2aa4143a
--- /dev/null
+++ b/src/main/java/io/ebeaninternal/server/transaction/TableModState.java
@@ -0,0 +1,51 @@
+package io.ebeaninternal.server.transaction;
+
+import io.ebean.cache.QueryCacheEntry;
+import io.ebean.cache.QueryCacheEntryValidate;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Holds timestamp of last modification per table.
+ *
+ * This information is used to validate entries in the L2 query caches.
+ *
+ */
+public class TableModState implements QueryCacheEntryValidate {
+
+ private Map tableModStamp = new ConcurrentHashMap<>();
+
+ /**
+ * Set the modified timestamp on the tables that have been touched.
+ */
+ public void touch(Set touchedTables, long modTimestamp) {
+
+ for (String tableName : touchedTables) {
+ tableModStamp.put(tableName, modTimestamp);
+ }
+ }
+
+ /**
+ * Return true if all the tables are valid based on timestamp comparison.
+ */
+ public boolean isValid(Set tables, long sinceTimestamp) {
+ for (String tableName : tables) {
+ Long modTime = tableModStamp.get(tableName);
+ if (modTime != null && modTime > sinceTimestamp ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isValid(QueryCacheEntry entry) {
+ Set dependentTables = entry.getDependentTables();
+ if (dependentTables != null && !dependentTables.isEmpty()) {
+ return isValid(dependentTables, entry.getTimestamp());
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
index 0e9c91d62..0ff2894d6 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java
@@ -46,7 +46,7 @@ import java.util.Set;
/**
* Manages transactions.
*
- * Keeps the Cache and Cluster in synch when transactions are committed.
+ * Keeps the Cache and Cluster in sync when transactions are committed.
*
*/
public class TransactionManager implements SpiTransactionManager {
@@ -136,6 +136,8 @@ public class TransactionManager implements SpiTransactionManager {
private final TimedMetricMap txnNamed;
private final TransactionScopeManager scopeManager;
+ private final TableModState tableModState;
+
/**
* Create the TransactionManager
*/
@@ -157,6 +159,7 @@ public class TransactionManager implements SpiTransactionManager {
this.clusterManager = options.clusterManager;
this.serverName = options.config.getName();
this.scopeManager = options.scopeManager;
+ this.tableModState = options.tableModState;
this.backgroundExecutor = options.backgroundExecutor;
this.dataSourceSupplier = options.dataSourceSupplier;
this.docStoreActive = options.config.getDocStoreConfig().isActive();
@@ -178,6 +181,13 @@ public class TransactionManager implements SpiTransactionManager {
scopeManager.register(this);
}
+ /**
+ * Return the NOW timestamp in epoch millis.
+ */
+ public long clockNowEpoch() {
+ return System.currentTimeMillis(); // TODO: Review when we supply a Clock via ServerConfig.
+ }
+
/**
* Create a new scoped transaction.
*/
@@ -434,7 +444,7 @@ public class TransactionManager implements SpiTransactionManager {
private void externalModificationEvent(TransactionEventTable tableEvents) {
- TransactionEvent event = new TransactionEvent();
+ TransactionEvent event = new TransactionEvent(clockNowEpoch());
event.add(tableEvents);
PostCommitProcessing postCommit = new PostCommitProcessing(clusterManager, this, event);
@@ -495,9 +505,10 @@ public class TransactionManager implements SpiTransactionManager {
/**
* Invalidate the query caches for entities based on views.
*/
- public void processViewInvalidation(Set viewInvalidation) {
- if (!viewInvalidation.isEmpty()) {
- beanDescriptorManager.processViewInvalidation(viewInvalidation);
+ public void processTouchedTables(Set touchedTables, long modTimestamp) {
+ tableModState.touch(touchedTables, modTimestamp);
+ if (viewInvalidation) {
+ beanDescriptorManager.processViewInvalidation(touchedTables);
}
}
diff --git a/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java b/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
index fe8b8d217..16f5dafe9 100644
--- a/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
+++ b/src/main/java/io/ebeaninternal/server/transaction/TransactionManagerOptions.java
@@ -24,10 +24,13 @@ public class TransactionManagerOptions {
final SpiProfileHandler profileHandler;
final TransactionScopeManager scopeManager;
final SpiLogManager logManager;
+ final TableModState tableModState;
+
public TransactionManagerOptions(boolean notifyL2CacheInForeground, ServerConfig config, TransactionScopeManager scopeManager, ClusterManager clusterManager,
BackgroundExecutor backgroundExecutor, DocStoreUpdateProcessor docStoreUpdateProcessor,
- BeanDescriptorManager descMgr, DataSourceSupplier dataSourceSupplier, SpiProfileHandler profileHandler, SpiLogManager logManager) {
+ BeanDescriptorManager descMgr, DataSourceSupplier dataSourceSupplier, SpiProfileHandler profileHandler,
+ SpiLogManager logManager, TableModState tableModState) {
this.notifyL2CacheInForeground = notifyL2CacheInForeground;
this.config = config;
@@ -39,6 +42,7 @@ public class TransactionManagerOptions {
this.dataSourceSupplier = dataSourceSupplier;
this.profileHandler = profileHandler;
this.logManager = logManager;
+ this.tableModState = tableModState;
}
}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java
index 3094c9ad5..046ae15d6 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolderTest.java
@@ -3,6 +3,8 @@ package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheFactory;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
+import io.ebean.config.ServerConfig;
+import io.ebeaninternal.server.transaction.TableModState;
import org.tests.model.basic.Contact;
import org.tests.model.basic.Customer;
import org.junit.Test;
@@ -17,10 +19,17 @@ public class DefaultCacheHolderTest {
private final ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
private final ServerCacheOptions defaultOptions = new ServerCacheOptions();
- @Test
- public void getCache_normal() throws Exception {
+ private CacheManagerOptions options() {
+ return new CacheManagerOptions(null, new ServerConfig(), true)
+ .with(defaultOptions, defaultOptions)
+ .with(cacheFactory, new TableModState());
+ }
- DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, null);
+
+ @Test
+ public void getCache_normal() {
+
+ DefaultCacheHolder holder = new DefaultCacheHolder(options());
DefaultServerCache cache = cache(holder, Customer.class, "customer");
assertThat(cache.getName()).isEqualTo("customer_B");
@@ -40,7 +49,9 @@ public class DefaultCacheHolderTest {
@Test
public void getCache_multiTenant() throws Exception {
- DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, tenantId::get);
+ CacheManagerOptions builder = options().with(tenantId::get);
+
+ DefaultCacheHolder holder = new DefaultCacheHolder(builder);
tenantId.set("ten_1");
DefaultServerCache cache = cache(holder, Customer.class, "customer");
@@ -48,26 +59,26 @@ public class DefaultCacheHolderTest {
cache.put("1", "value-for-tenant1");
cache.put("2", "an other value-for-tenant1");
-
+
assertThat(cache.size()).isEqualTo(2);
-
+
tenantId.set("ten_2");
cache.put("1", "value-for-tenant2");
cache.put("2", "an other value-for-tenant2");
-
+
assertThat(cache.size()).isEqualTo(4);
-
+
assertThat(cache.get("1")).isEqualTo("value-for-tenant2");
assertThat(cache.get("2")).isEqualTo("an other value-for-tenant2");
-
-
+
+
tenantId.set("ten_1");
-
+
assertThat(cache.get("1")).isEqualTo("value-for-tenant1");
assertThat(cache.get("2")).isEqualTo("an other value-for-tenant1");
-
- Exception exInThread[] = new Exception[1];
+
+ Exception exInThread[] = new Exception[1];
Thread t = new Thread() {
@Override
public void run() {
@@ -77,29 +88,29 @@ public class DefaultCacheHolderTest {
cache.put("1", "value-for-tenant2");
cache.put("2", "an other value-for-tenant2");
-
+
tenantId.set(null);
-
+
cache.clear();
} catch (Exception e) {
exInThread[0] = e;
}
};
};
-
+
// do some async work
t.start();
t.join();
- if (exInThread[0] != null) {
+ if (exInThread[0] != null) {
throw exInThread[0];
}
assertThat(cache.size()).isEqualTo(0);
}
-
@Test
- public void clearAll() throws Exception {
- DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, null);
+ public void clearAll() {
+
+ DefaultCacheHolder holder = new DefaultCacheHolder(options());
DefaultServerCache cache = cache(holder, Customer.class, "customer");
cache.put("foo", "foo");
assertThat(cache.size()).isEqualTo(1);
@@ -109,8 +120,11 @@ public class DefaultCacheHolderTest {
}
@Test
- public void clearAll_multiTenant() throws Exception {
- DefaultCacheHolder holder = new DefaultCacheHolder(cacheFactory, defaultOptions, defaultOptions, tenantId::get);
+ public void clearAll_multiTenant() {
+
+ CacheManagerOptions options = options().with(tenantId::get);
+
+ DefaultCacheHolder holder = new DefaultCacheHolder(options);
DefaultServerCache cache = cache(holder, Customer.class, "customer");
cache.put("foo", "foo");
assertThat(cache.size()).isEqualTo(1);
@@ -118,5 +132,5 @@ public class DefaultCacheHolderTest {
holder.clearAll();
assertThat(cache.size()).isEqualTo(0);
}
-
+
}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java
index beefe1f47..6cbc82c8f 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultCacheHolder_getCacheOptions_Test.java
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.cache;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
+import io.ebean.config.ServerConfig;
import org.tests.model.basic.Article;
import org.tests.model.basic.Order;
import org.tests.model.basic.Product;
@@ -19,7 +20,10 @@ public class DefaultCacheHolder_getCacheOptions_Test {
defaultOptions.setMaxSize(10000);
defaultOptions.setMaxSecsToLive(120);
- this.cacheHolder = new DefaultCacheHolder(null, defaultOptions, defaultOptions, null);
+ CacheManagerOptions builder = new CacheManagerOptions(null, new ServerConfig(), true)
+ .with(defaultOptions, defaultOptions);
+
+ this.cacheHolder = new DefaultCacheHolder(builder);
}
@Test
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheConfigTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheConfigTest.java
new file mode 100644
index 000000000..bb03cce18
--- /dev/null
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheConfigTest.java
@@ -0,0 +1,52 @@
+package io.ebeaninternal.server.cache;
+
+import io.ebean.cache.ServerCacheConfig;
+import io.ebean.cache.ServerCacheOptions;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class DefaultServerCacheConfigTest {
+
+
+ private DefaultServerCacheConfig create(int maxSize, int maxIdleSecs, int maxSecsToLive, int trimFreq) {
+ ServerCacheOptions options = new ServerCacheOptions();
+ options.setMaxSize(maxSize);
+ options.setMaxIdleSecs(maxIdleSecs);
+ options.setMaxSecsToLive(maxSecsToLive);
+ options.setTrimFrequency(trimFreq);
+
+ return new DefaultServerCacheConfig(new ServerCacheConfig(null, null, options, null, null));
+ }
+
+ @Test
+ public void trimFreq_halfIdle() {
+
+ assertEquals(create(10000,10,20, 0).determineTrimFrequency(), 4);
+ }
+
+
+ @Test
+ public void trimFreq_halfIdle_withRounding() {
+
+ assertEquals(create(10000,11,20, 0).determineTrimFrequency(), 4);
+ }
+
+ @Test
+ public void trimFreq_halfTTL() {
+
+ assertEquals(create(10000,0,20, 0).determineTrimFrequency(), 9);
+ }
+
+ @Test
+ public void trimFreq_halfTTL_withRounding() {
+
+ assertEquals(create(10000,0,21, 0).determineTrimFrequency(), 9);
+ }
+
+ @Test
+ public void trimFreq_explicit() {
+
+ assertEquals(create(10000,10,20, 42).determineTrimFrequency(), 42);
+ }
+}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java
index 00ff68631..ead5158ff 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCacheTest.java
@@ -1,6 +1,8 @@
package io.ebeaninternal.server.cache;
+import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
+import io.ebean.cache.ServerCacheType;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -15,11 +17,13 @@ public class DefaultServerCacheTest {
cacheOptions.setMaxSecsToLive(600);
cacheOptions.setTrimFrequency(60);
- return new DefaultServerCache("foo", null, cacheOptions);
+ ServerCacheConfig con = new ServerCacheConfig(ServerCacheType.BEAN, "foo", cacheOptions, null, null);
+ DefaultServerCacheConfig config = new DefaultServerCacheConfig(con);
+ return new DefaultServerCache(config);
}
@Test
- public void testGetHitRatio() throws Exception {
+ public void testGetHitRatio() {
DefaultServerCache cache = createCache();
assertEquals(0, cache.getHitRatio());
@@ -34,7 +38,7 @@ public class DefaultServerCacheTest {
}
@Test
- public void testSize() throws Exception {
+ public void testSize() {
DefaultServerCache cache = createCache();
assertEquals(0, cache.size());
@@ -50,39 +54,5 @@ public class DefaultServerCacheTest {
assertEquals(0, cache.size());
}
- @Test
- public void trimFreq_halfIdle() throws Exception {
-
- DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 10, 20, 0);
- assertEquals(cache.trimFrequency, 4);
- }
-
- @Test
- public void trimFreq_halfIdle_withRounding() throws Exception {
-
- DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 11, 20, 0);
- assertEquals(cache.trimFrequency, 4);
- }
-
- @Test
- public void trimFreq_halfTTL() throws Exception {
-
- DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 0, 20, 0);
- assertEquals(cache.trimFrequency, 9);
- }
-
- @Test
- public void trimFreq_halfTTL_withRounding() throws Exception {
-
- DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 0, 21, 0);
- assertEquals(cache.trimFrequency, 9);
- }
-
- @Test
- public void trimFreq_explicit() throws Exception {
-
- DefaultServerCache cache = new DefaultServerCache("", null, null, 10000, 10, 20, 42);
- assertEquals(cache.trimFrequency, 42);
- }
}
diff --git a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java
index 4ba1d82a4..a2d630c98 100644
--- a/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java
+++ b/src/test/java/io/ebeaninternal/server/cache/DefaultServerCache_RunEvictionTest.java
@@ -1,7 +1,9 @@
package io.ebeaninternal.server.cache;
+import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheStatistics;
+import io.ebean.cache.ServerCacheType;
import org.junit.Ignore;
import org.junit.Test;
@@ -19,7 +21,8 @@ public class DefaultServerCache_RunEvictionTest {
cacheOptions.setMaxSecsToLive(2);
cacheOptions.setTrimFrequency(1);
- return new DefaultServerCache("foo", null, cacheOptions);
+ ServerCacheConfig con = new ServerCacheConfig(ServerCacheType.BEAN, "foo", cacheOptions, null, null);
+ return new DefaultServerCache(new DefaultServerCacheConfig(con));
}
private final DefaultServerCache cache;
diff --git a/src/test/java/io/ebeaninternal/server/transaction/TableModStateTest.java b/src/test/java/io/ebeaninternal/server/transaction/TableModStateTest.java
new file mode 100644
index 000000000..5abcb1820
--- /dev/null
+++ b/src/test/java/io/ebeaninternal/server/transaction/TableModStateTest.java
@@ -0,0 +1,44 @@
+package io.ebeaninternal.server.transaction;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class TableModStateTest {
+
+ private TableModState tableModState = new TableModState();
+
+ @Test
+ public void isValid() {
+
+ long now = System.currentTimeMillis();
+
+ tableModState.touch(setOf("one", "two", "three"), now);
+
+ // empty
+ assertTrue(tableModState.isValid(Collections.emptySet(), 12L));
+
+ // no entry
+ assertTrue(tableModState.isValid(setOf("noEntry"), 12L));
+
+ // later timestamp
+ assertTrue(tableModState.isValid(setOf("one"), now + 1));
+ assertTrue(tableModState.isValid(setOf("one", "two", "noEntry"), now + 1));
+
+ // invalid
+ assertFalse(tableModState.isValid(setOf("one"), now - 1));
+ assertFalse(tableModState.isValid(setOf("one", "two"), now - 1));
+ assertFalse(tableModState.isValid(setOf("three", "two"), now - 1));
+
+ }
+
+ private Set setOf(String... tables) {
+ Set touched = new HashSet<>();
+ Collections.addAll(touched, tables);
+ return touched;
+ }
+}
diff --git a/src/test/java/org/tests/cache/TestQueryCacheTableDependency.java b/src/test/java/org/tests/cache/TestQueryCacheTableDependency.java
new file mode 100644
index 000000000..f578fdfdf
--- /dev/null
+++ b/src/test/java/org/tests/cache/TestQueryCacheTableDependency.java
@@ -0,0 +1,59 @@
+package org.tests.cache;
+
+import io.ebean.BaseTestCase;
+import io.ebean.Ebean;
+import io.ebean.cache.ServerCache;
+import org.junit.Test;
+import org.tests.model.basic.Address;
+import org.tests.model.basic.Customer;
+import org.tests.model.basic.ResetBasicData;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestQueryCacheTableDependency extends BaseTestCase {
+
+ @Test
+ public void testFindCountOnDependent() {
+
+ ResetBasicData.reset();
+
+ ServerCache customerCache = Ebean.getServerCacheManager().getQueryCache(Customer.class);
+ customerCache.clear();
+
+ List addrs = Ebean.find(Address.class)
+ .where().eq("line2", "St Lukes")
+ .findList();
+
+ int custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
+ .where().eq("billingAddress.line2", "St Lukes")
+ .findCount();
+
+ assertThat(custs).isEqualTo(3);
+
+ custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
+ .where().eq("billingAddress.line2", "St Lukes")
+ .findCount();
+
+ assertThat(custs).isEqualTo(3);
+
+ Address a1 = addrs.get(0);
+ a1.setLine2("St Lucky");
+ Ebean.save(a1);
+
+ custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
+ .where().eq("billingAddress.line2", "St Lukes")
+ .findCount();
+
+ assertThat(custs).isEqualTo(2); // cache says 3
+
+
+ custs = Ebean.find(Customer.class).setUseQueryCache(true).setReadOnly(true)
+ .where().eq("billingAddress.line2", "St Lucky")
+ .findCount();
+
+ assertThat(custs).isEqualTo(1);
+
+ }
+}
diff --git a/src/test/java/org/tests/model/basic/Address.java b/src/test/java/org/tests/model/basic/Address.java
index 83d1b5c18..a11d38a0d 100644
--- a/src/test/java/org/tests/model/basic/Address.java
+++ b/src/test/java/org/tests/model/basic/Address.java
@@ -1,5 +1,6 @@
package org.tests.model.basic;
+import io.ebean.annotation.InvalidateQueryCache;
import org.tests.model.basic.metaannotation.SizeMedium;
import javax.persistence.Column;
@@ -14,6 +15,11 @@ import java.sql.Timestamp;
/**
* Address entity bean.
*/
+// Address is not L2 cached directly but it is joined to queries that are cached
+// What InvalidateQueryCache means is that we propagate a table modification event
+// when address is changed ... and cached queries that join to address will be
+// invalidated accordingly.
+@InvalidateQueryCache
@Entity
@Table(name = "o_address")
public class Address {