diff --git a/src/main/java/com/samskivert/depot/CacheAdapter.java b/src/main/java/com/samskivert/depot/CacheAdapter.java index 8ab3377..a1ef076 100644 --- a/src/main/java/com/samskivert/depot/CacheAdapter.java +++ b/src/main/java/com/samskivert/depot/CacheAdapter.java @@ -58,6 +58,14 @@ public interface CacheAdapter */ public Iterable enumerate (String cacheId); + /** + * Clears the cache with the specified name. + * + * @param localOnly if true, only clear the cache on the local node, do not broadcast + * instructions for all distributed nodes to clear this cache as well. + */ + public void clear (String cacheId, boolean localOnly); + /** * Shut down all operations, e.g. persisting memory contents to disk. */ diff --git a/src/main/java/com/samskivert/depot/DepotRepository.java b/src/main/java/com/samskivert/depot/DepotRepository.java index 775c7b7..2593c81 100644 --- a/src/main/java/com/samskivert/depot/DepotRepository.java +++ b/src/main/java/com/samskivert/depot/DepotRepository.java @@ -240,50 +240,7 @@ public abstract class DepotRepository Class type, CacheStrategy cache, Iterable clauses) throws DatabaseException { - DepotMarshaller marsh = _ctx.getMarshaller(type); - - switch (cache) { - case LONG_KEYS: case SHORT_KEYS: case BEST: case RECORDS: - String reason = null; - if (marsh.getTableName() == null) { - reason = type + " is computed"; - - } else if (!marsh.hasPrimaryKey()) { - reason = type + " has no primary key"; - - } else { - for (QueryClause clause : clauses) { - if (clause instanceof FieldOverride) { - reason = "query uses a FieldOverride clause"; - break; - } - } - } - if (cache == CacheStrategy.BEST) { - cache = (reason != null) ? CacheStrategy.NONE : CacheStrategy.SHORT_KEYS; - } else if (reason != null) { - // if user explicitly asked for a strategy we can't do, protest - throw new IllegalArgumentException( - "Cannot use " + cache + " strategy because " + reason); - } - break; - - case NONE: case CONTENTS: - break; // NONE and CONTENTS can always be used. - } - - if (!_ctx.isUsingCache()) { - cache = CacheStrategy.NONE; - } - - switch (cache) { - case SHORT_KEYS: case LONG_KEYS: case RECORDS: - return _ctx.invoke(new FindAllQuery.WithCache(_ctx, type, clauses, cache)); - - default: - return _ctx.invoke(new FindAllQuery.Explicitly( - _ctx, type, clauses, cache == CacheStrategy.CONTENTS)); - } + return _ctx.invoke(FindAllQuery.newCachedFullRecordQuery(_ctx, type, cache, clauses)); } /** diff --git a/src/main/java/com/samskivert/depot/EHCacheAdapter.java b/src/main/java/com/samskivert/depot/EHCacheAdapter.java index 98d6c3b..039fe84 100644 --- a/src/main/java/com/samskivert/depot/EHCacheAdapter.java +++ b/src/main/java/com/samskivert/depot/EHCacheAdapter.java @@ -173,6 +173,15 @@ public class EHCacheAdapter return result; } + // from CacheAdapter + public void clear (String cacheId, boolean localOnly) + { + EHCacheBin bin = _bins.get(cacheId); + if (bin != null) { + bin.getCache().removeAll(localOnly); + } + } + // from CacheAdapter public void shutdown () { diff --git a/src/main/java/com/samskivert/depot/PersistenceContext.java b/src/main/java/com/samskivert/depot/PersistenceContext.java index abf00a9..37180f3 100644 --- a/src/main/java/com/samskivert/depot/PersistenceContext.java +++ b/src/main/java/com/samskivert/depot/PersistenceContext.java @@ -425,6 +425,33 @@ public class PersistenceContext } } + /** + * Requests that the cache for the specified persistent record be cleared. Note that this + * clears only the by-primary-key cache for the index in question. It does not clear + * the keyset or contents query caches. Use {@link Query#clearCache} to clear + * these caches. + * + * @param localOnly if true, only the cache in this JVM will be cleared, no broadcast message + * will be sent to instruct all distributed nodes to also clear this cache. + */ + public void cacheClear (Class pClass, boolean localOnly) + { + cacheClear(pClass.getName(), localOnly); + } + + /** + * Requests that the cache with the specified id be cleared. + * + * @param localOnly if true, only the cache in this JVM will be cleared, no broadcast message + * will be sent to instruct all distributed nodes to also clear this cache. + */ + public void cacheClear (String cacheId, boolean localOnly) + { + if (_cache != null) { + _cache.clear(cacheId, localOnly); + } + } + /** * Registers a new cache listener with the cache associated with the given class. */ diff --git a/src/main/java/com/samskivert/depot/Query.java b/src/main/java/com/samskivert/depot/Query.java index 49495ea..5970f33 100644 --- a/src/main/java/com/samskivert/depot/Query.java +++ b/src/main/java/com/samskivert/depot/Query.java @@ -555,6 +555,37 @@ public class Query return _repo.deleteAll(_pclass, _where, _limit, invalidator); } + /** + * Clears the cache for the query configured by this instance. If the query would not result in + * cache usage for whatever reason, this will NOOP. + * + *

Note: Depot uses numerous caches, so it is important to know what you are doing. If the + * records fetched by this query have a primary key, Depot performs a two-phase query where the + * primary keys for the records that satisify this query are first fetched and stored in a + * keyset cache, then the actual records needed to satisfy the query are fetched using + * the by-primary-key cache. This method will only clear the keyset cache. + * That may be sufficient for your purposes, or you may need to call {@link + * PersistenceContext#cacheClear(Class,boolean)} to clear the by-primary-key cache as + * well. If the record in question does not define a primary key, Depot will use a + * contents cache to store the entire records fetched as a result of executing this + * query. In that case, calling this method is sufficient to ensure that no cached data will be + * used to fulfill subsequent similarly configured queries.

+ * + *

Finally, note that query configuration other than {@link #cache} (i.e. {@link #limit}, + * {@link #groupBy}, {@link #join}, etc.) will have no influence on this operation.

+ * + * @param localOnly if true, only the cache in this JVM will be cleared, no broadcast message + * will be sent to instruct all distributed nodes to also clear this cache. + */ + public void clearCache (boolean localOnly) + { + String cacheId = FindAllQuery.newCachedFullRecordQuery( + _ctx, _pclass, _cache, getClauses()).getCacheId(); + if (cacheId != null) { + _ctx.cacheClear(cacheId, localOnly); + } + } + protected Query (PersistenceContext ctx, DepotRepository repo, Class pclass) { _ctx = ctx; diff --git a/src/main/java/com/samskivert/depot/impl/FindAllQuery.java b/src/main/java/com/samskivert/depot/impl/FindAllQuery.java index 193d1e1..2117113 100644 --- a/src/main/java/com/samskivert/depot/impl/FindAllQuery.java +++ b/src/main/java/com/samskivert/depot/impl/FindAllQuery.java @@ -47,24 +47,66 @@ public abstract class FindAllQuery extends Fetcher> { /** - * A base class for queries that fetch a full record at a time. + * A base class for cached queries that fetch a full record at a time. */ - public static abstract class FullRecordQuery - extends FindAllQuery + public static abstract class CachedFullRecordQuery + extends FullRecordQuery { - protected FullRecordQuery (PersistenceContext ctx, Class type) { - super(type, ctx.getMarshaller(type), new CloningCloner()); - _dmarsh = ctx.getMarshaller(type); + protected CachedFullRecordQuery (PersistenceContext ctx, Class type) { + super(ctx, type); } - protected DepotMarshaller _dmarsh; + /** + * Returns the id of the cache in which this query's results will be stored, or null if the + * query is not configured to use a cache. + */ + public String getCacheId () { + return (_qkey == null) ? null : _qkey.getCacheId(); + } + + protected SimpleCacheKey _qkey; } /** * The two-pass collection query implementation. See {@link DepotRepository#findAll} for * details. */ - public static class WithCache extends FullRecordQuery + public static class WithKeys extends FullRecordQuery + { + public WithKeys (PersistenceContext ctx, Iterable> keys) + throws DatabaseException + { + super(ctx, keys.iterator().next().getPersistentClass()); + _keys = keys; + } + + @Override // from Fetcher + public List getCachedResult (PersistenceContext ctx) + { + // look up what we can from the cache + _fetchKeys = loadFromCache(ctx, _keys, _entities); + + // if we found everything, we can just return our result straight away, yay! + return _fetchKeys.isEmpty() ? resolve(_keys, _entities) : null; + } + + // from Fetcher + public List invoke (PersistenceContext ctx, Connection conn, DatabaseLiaison liaison) + throws SQLException + { + return loadAndResolve(ctx, conn, _keys, _fetchKeys, _entities, null); + } + + protected Iterable> _keys; + protected Set> _fetchKeys; + protected Map, T> _entities = Maps.newHashMap(); + } + + /** + * The two-pass collection query implementation. See {@link DepotRepository#findAll} for + * details. + */ + public static class WithCache extends CachedFullRecordQuery { public WithCache (PersistenceContext ctx, Class type, Iterable clauses, CacheStrategy strategy) @@ -94,7 +136,6 @@ public abstract class FindAllQuery default: throw new IllegalArgumentException("Unexpected cache strategy: " + strategy); } - } @Override // from Fetcher @@ -148,7 +189,6 @@ public abstract class FindAllQuery return loadAndResolve(ctx, conn, _keys, _fetchKeys, _entities, stmtString); } - protected SimpleCacheKey _qkey; protected CacheCategory _category; protected SelectClause _select; protected KeySet _keys; @@ -156,60 +196,20 @@ public abstract class FindAllQuery protected Map, T> _entities = Maps.newHashMap(); } - /** - * The two-pass collection query implementation. See {@link DepotRepository#findAll} for - * details. - */ - public static class WithKeys extends FullRecordQuery - { - public WithKeys (PersistenceContext ctx, Iterable> keys) - throws DatabaseException - { - super(ctx, keys.iterator().next().getPersistentClass()); - _keys = keys; - } - - @Override // from Fetcher - public List getCachedResult (PersistenceContext ctx) - { - // look up what we can from the cache - _fetchKeys = loadFromCache(ctx, _keys, _entities); - - // if we found everything, we can just return our result straight away, yay! - return _fetchKeys.isEmpty() ? resolve(_keys, _entities) : null; - } - - // from Fetcher - public List invoke (PersistenceContext ctx, Connection conn, DatabaseLiaison liaison) - throws SQLException - { - return loadAndResolve(ctx, conn, _keys, _fetchKeys, _entities, null); - } - - protected Iterable> _keys; - protected Set> _fetchKeys; - protected Map, T> _entities = Maps.newHashMap(); - } - /** * The single-pass collection query implementation. See {@link DepotRepository#findAll} for * details. */ - public static class Explicitly extends FullRecordQuery + public static class Explicitly extends CachedFullRecordQuery { public Explicitly (PersistenceContext ctx, Class type, Iterable clauses, boolean cachedContents) throws DatabaseException { super(ctx, type); - _select = new SelectClause(type, _dmarsh.getSelections(), clauses); - - if (cachedContents) { - _qkey = new SimpleCacheKey(_dmarsh.getTableName() + "Contents", _select.toString()); - } else { - _qkey = null; - } + _qkey = !cachedContents ? null : + new SimpleCacheKey(_dmarsh.getTableName() + "Contents", _select.toString()); } @Override // from Fetcher @@ -246,7 +246,6 @@ public abstract class FindAllQuery } protected SelectClause _select; - protected SimpleCacheKey _qkey; } public static class Projection @@ -286,47 +285,51 @@ public abstract class FindAllQuery protected DepotTypes _types; } - protected static class ProjectionQueryMarshaller - implements QueryMarshaller + public static CachedFullRecordQuery newCachedFullRecordQuery ( + PersistenceContext ctx, Class type, CacheStrategy strategy, + Iterable clauses) { - public ProjectionQueryMarshaller (Projector cset, DepotTypes types) { - _cset = cset; - _types = types; - } + DepotMarshaller marsh = ctx.getMarshaller(type); - public String getTableName () { - return _types.getTableName(_cset.ptype); - } + switch (strategy) { + case LONG_KEYS: case SHORT_KEYS: case BEST: case RECORDS: + String reason = null; + if (marsh.getTableName() == null) { + reason = type + " is computed"; - public SQLExpression[] getSelections () { - return _cset.selexps; - } + } else if (!marsh.hasPrimaryKey()) { + reason = type + " has no primary key"; - public Key getPrimaryKey (Object object) { - return _types.getMarshaller(_cset.ptype).getPrimaryKey(object); - } - - public R createObject (ResultSet rs) throws SQLException { - Object[] data = new Object[_cset.selexps.length]; - for (int ii = 0; ii < data.length; ii++) { - SQLExpression exp = _cset.selexps[ii]; - if (exp instanceof ColumnExp) { - ColumnExp col = (ColumnExp)exp; - data[ii] = _types.getMarshaller(col.getPersistentClass()). - getFieldMarshaller(col.name).getFromSet(rs, ii+1); - } else { - // TEMP: in the case of selecting computed expressions, we rely on the types to - // be correct by construction; TODO: this will probably bite us when JDBC - // drivers choose Long instead of Integer or whatnot, so we'll need to set up - // more complex machinery - data[ii] = rs.getObject(ii+1); + } else { + for (QueryClause clause : clauses) { + if (clause instanceof FieldOverride) { + reason = "query uses a FieldOverride clause"; + break; + } } } - return _cset.createObject(data); + if (strategy == CacheStrategy.BEST) { + strategy = (reason != null) ? CacheStrategy.NONE : CacheStrategy.SHORT_KEYS; + } else if (reason != null) { + // if user explicitly asked for a strategy we can't do, protest + throw new IllegalArgumentException( + "Cannot use " + strategy + " strategy because " + reason); + } + break; + + case NONE: case CONTENTS: + break; // NONE and CONTENTS can always be used. } - protected Projector _cset; - protected DepotTypes _types; + // if we're not using a cache, then skip all of the above + strategy = ctx.isUsingCache() ? strategy : CacheStrategy.NONE; + + switch (strategy) { + case SHORT_KEYS: case LONG_KEYS: case RECORDS: + return new WithCache(ctx, type, clauses, strategy); + default: + return new Explicitly(ctx, type, clauses, strategy == CacheStrategy.CONTENTS); + } } // from Fetcher @@ -457,6 +460,62 @@ public abstract class FindAllQuery return builder.append(")").toString(); } + /** A base class for queries that fetch a full record at a time. */ + protected static abstract class FullRecordQuery + extends FindAllQuery + { + protected FullRecordQuery (PersistenceContext ctx, Class type) { + super(type, ctx.getMarshaller(type), new CloningCloner()); + _dmarsh = ctx.getMarshaller(type); + } + + protected DepotMarshaller _dmarsh; + } + + /** Helper for {@link Projection}. */ + protected static class ProjectionQueryMarshaller + implements QueryMarshaller + { + public ProjectionQueryMarshaller (Projector cset, DepotTypes types) { + _cset = cset; + _types = types; + } + + public String getTableName () { + return _types.getTableName(_cset.ptype); + } + + public SQLExpression[] getSelections () { + return _cset.selexps; + } + + public Key getPrimaryKey (Object object) { + return _types.getMarshaller(_cset.ptype).getPrimaryKey(object); + } + + public R createObject (ResultSet rs) throws SQLException { + Object[] data = new Object[_cset.selexps.length]; + for (int ii = 0; ii < data.length; ii++) { + SQLExpression exp = _cset.selexps[ii]; + if (exp instanceof ColumnExp) { + ColumnExp col = (ColumnExp)exp; + data[ii] = _types.getMarshaller(col.getPersistentClass()). + getFieldMarshaller(col.name).getFromSet(rs, ii+1); + } else { + // TEMP: in the case of selecting computed expressions, we rely on the types to + // be correct by construction; TODO: this will probably bite us when JDBC + // drivers choose Long instead of Integer or whatnot, so we'll need to set up + // more complex machinery + data[ii] = rs.getObject(ii+1); + } + } + return _cset.createObject(data); + } + + protected Projector _cset; + protected DepotTypes _types; + } + // we have to factor out cloning data for cache storage so that we can support storing // immutable data (like Integer and String) which need not and cannot be cloned, versus mutable // data (like PersistentRecords and Tuples) which must be cloned diff --git a/src/test/java/com/samskivert/depot/TestCacheAdapter.java b/src/test/java/com/samskivert/depot/TestCacheAdapter.java index 1e69b0a..f7de358 100644 --- a/src/test/java/com/samskivert/depot/TestCacheAdapter.java +++ b/src/test/java/com/samskivert/depot/TestCacheAdapter.java @@ -6,6 +6,7 @@ package com.samskivert.depot; import java.io.Serializable; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -20,6 +21,57 @@ import com.samskivert.util.Tuple; */ public class TestCacheAdapter implements CacheAdapter { + // from interface CacheAdapter + public CacheAdapter.CachedValue lookup (String cacheId, Serializable key) + { + // System.err.println("GET " + key + ": " + _cache.containsKey(key)); + @SuppressWarnings("unchecked") + CachedValue value = (CachedValue) _cache.get( + new Tuple(cacheId, key)); + return value; + } + + // from interface CacheAdapter + public void store (CacheCategory category, String cacheId, Serializable key, T value) + { + // System.err.println("STORE " + key); + _cache.put(new Tuple(cacheId, key), new TestCachedValue(value)); + } + + // from interface CacheAdapter + public void remove (String cacheId, Serializable key) + { + // System.err.println("REMOVE " + key); + _cache.remove(new Tuple(cacheId, key)); + } + + // from interface CacheAdapter + public Iterable enumerate (String cacheId) + { + // in a real implementation this would be a lazily constructed iterable + List result = Lists.newArrayList(); + for (Map.Entry, CachedValue> entry: _cache.entrySet()) { + if (entry.getKey().left.equals(cacheId)) { + result.add(entry.getKey().right); + } + } + return result; + } + + // from interface CacheAdapter + public void clear (String cacheId, boolean localOnly) + { + synchronized (_cache) { + for (Iterator> iter = _cache.keySet().iterator(); + iter.hasNext(); ) { + Tuple key = iter.next(); + if (key.left.equals(cacheId)) { + iter.remove(); + } + } + } + } + // from interface CacheAdapter public void shutdown () { @@ -37,33 +89,6 @@ public class TestCacheAdapter implements CacheAdapter protected final T _value; } - public CacheAdapter.CachedValue lookup (String cacheId, Serializable key) { - // System.err.println("GET " + key + ": " + _cache.containsKey(key)); - @SuppressWarnings("unchecked") - CachedValue value = (CachedValue) _cache.get( - new Tuple(cacheId, key)); - return value; - } - public void store (CacheCategory category, String cacheId, Serializable key, T value) { - // System.err.println("STORE " + key); - _cache.put(new Tuple(cacheId, key), new TestCachedValue(value)); - } - public void remove (String cacheId, Serializable key) { - // System.err.println("REMOVE " + key); - _cache.remove(new Tuple(cacheId, key)); - } - public Iterable enumerate (String cacheId) - { - // in a real implementation this would be a lazily constructed iterable - List result = Lists.newArrayList(); - for (Map.Entry, CachedValue> entry: _cache.entrySet()) { - if (entry.getKey().left.equals(cacheId)) { - result.add(entry.getKey().right); - } - } - return result; - } - protected Map, CachedValue> _cache = Collections.synchronizedMap( Maps., CachedValue>newHashMap());