--- /dev/null
+"""caching_query.py
+
+Represent persistence structures which allow the usage of
+Beaker caching with SQLAlchemy.
+
+The three new concepts introduced here are:
+
+ * CachingQuery - a Query subclass that caches and
+ retrieves results in/from Beaker.
+ * FromCache - a query option that establishes caching
+ parameters on a Query
+ * RelationshipCache - a variant of FromCache which is specific
+ to a query invoked during a lazy load.
+ * _params_from_query - extracts value parameters from
+ a Query.
+
+The rest of what's here are standard SQLAlchemy and
+Beaker constructs.
+
+"""
+from sqlalchemy.orm.interfaces import MapperOption
+from sqlalchemy.orm.query import Query
+from sqlalchemy.sql import visitors
+
+class CachingQuery(Query):
+ """A Query subclass which optionally loads full results from a Beaker
+ cache region.
+
+ The CachingQuery stores additional state that allows it to consult
+ a Beaker cache before accessing the database:
+
+ * A "region", which is a cache region argument passed to a
+ Beaker CacheManager
+ * A "namespace", which is a qualifying name that identifies a
+ group of keys within the cache. A query that filters on a name
+ might use the name "by_name", a query that filters on a date range
+ to a joined table might use the name "related_date_range".
+
+ When the above state is present, a Beaker cache is retrieved.
+
+ The "namespace" name is first concatenated with
+ a string composed of the individual entities and columns the Query
+ requests, i.e. such as ``Query(User.id, User.name)``.
+
+ The Beaker cache is then loaded from the cache manager based
+ on the region and composed namespace. The key within the cache
+ itself is then constructed against the bind parameters specified
+ by this query, which are usually literals defined in the
+ WHERE clause.
+
+ The FromCache and RelationshipCache mapper options below represent
+ the "public" method of configuring this state upon the CachingQuery.
+
+ """
+
+ def __init__(self, manager, *args, **kw):
+ self.cache_manager = manager
+ Query.__init__(self, *args, **kw)
+
+ def __iter__(self):
+ """override __iter__ to pull results from Beaker
+ if particular attributes have been configured.
+ """
+ if hasattr(self, '_cache_parameters'):
+ return self.get_value(createfunc=lambda: list(Query.__iter__(self)))
+ else:
+ return Query.__iter__(self)
+
+ def invalidate(self):
+ """Invalidate the cache represented in this Query."""
+
+ cache, cache_key = _get_cache_parameters(self)
+ cache.remove(cache_key)
+
+ def get_value(self, merge=True, createfunc=None):
+ """Return the value from the cache for this query.
+
+ Returns None if no value present.
+
+ """
+ cache, cache_key = _get_cache_parameters(self)
+ ret = cache.get_value(cache_key, createfunc=createfunc)
+ if merge:
+ ret = self.merge_result(ret, load=False)
+ return ret
+
+ def set_value(self, value):
+ """Set the value in the cache for this query."""
+
+ cache, cache_key = _get_cache_parameters(self)
+ cache.put(cache_key, value)
+
+def query_callable(manager):
+ def query(*arg, **kw):
+ return CachingQuery(manager, *arg, **kw)
+ return query
+
+def _get_cache_parameters(query):
+ """For a query with cache_region and cache_namespace configured,
+ return the correspoinding Cache instance and cache key, based
+ on this query's current criterion and parameter values.
+
+ """
+ if not hasattr(query, '_cache_parameters'):
+ raise ValueError("This Query does not have caching parameters configured.")
+
+ region, namespace, cache_key = query._cache_parameters
+
+ namespace = _namespace_from_query(namespace, query)
+
+ if cache_key is None:
+ # cache key - the value arguments from this query's parameters.
+ args = _params_from_query(query)
+ cache_key = " ".join([str(x) for x in args])
+
+ # get cache
+ cache = query.cache_manager.get_cache_region(namespace, region)
+
+ # optional - hash the cache_key too for consistent length
+ # import uuid
+ # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key))
+
+ return cache, cache_key
+
+def _namespace_from_query(namespace, query):
+ # cache namespace - the token handed in by the
+ # option + class we're querying against
+ namespace = " ".join([namespace] + [str(x) for x in query._entities])
+
+ # memcached wants this
+ namespace = namespace.replace(' ', '_')
+
+ return namespace
+
+def _set_cache_parameters(query, region, namespace, cache_key):
+
+ if hasattr(query, '_cache_parameters'):
+ region, namespace, cache_key = query._cache_parameters
+ raise ValueError("This query is already configured "
+ "for region %r namespace %r" %
+ (region, namespace)
+ )
+ query._cache_parameters = region, namespace, cache_key
+
+class FromCache(MapperOption):
+ """Specifies that a Query should load results from a cache."""
+
+ propagate_to_loaders = False
+
+ def __init__(self, region, namespace, cache_key=None):
+ """Construct a new FromCache.
+
+ :param region: the cache region. Should be a
+ region configured in the Beaker CacheManager.
+
+ :param namespace: the cache namespace. Should
+ be a name uniquely describing the target Query's
+ lexical structure.
+
+ :param cache_key: optional. A string cache key
+ that will serve as the key to the query. Use this
+ if your query has a huge amount of parameters (such
+ as when using in_()) which correspond more simply to
+ some other identifier.
+
+ """
+ self.region = region
+ self.namespace = namespace
+ self.cache_key = cache_key
+
+ def process_query(self, query):
+ """Process a Query during normal loading operation."""
+
+ _set_cache_parameters(query, self.region, self.namespace, self.cache_key)
+
+class RelationshipCache(MapperOption):
+ """Specifies that a Query as called within a "lazy load"
+ should load results from a cache."""
+
+ propagate_to_loaders = True
+
+ def __init__(self, region, namespace, attribute):
+ """Construct a new RelationshipCache.
+
+ :param region: the cache region. Should be a
+ region configured in the Beaker CacheManager.
+
+ :param namespace: the cache namespace. Should
+ be a name uniquely describing the target Query's
+ lexical structure.
+
+ :param attribute: A Class.attribute which
+ indicates a particular class relationship() whose
+ lazy loader should be pulled from the cache.
+
+ """
+ self.region = region
+ self.namespace = namespace
+ self._relationship_options = {
+ ( attribute.property.parent.class_, attribute.property.key ) : self
+ }
+
+ def process_query_conditionally(self, query):
+ """Process a Query that is used within a lazy loader.
+
+ (the process_query_conditionally() method is a SQLAlchemy
+ hook invoked only within lazyload.)
+
+ """
+ if query._current_path:
+ mapper, key = query._current_path[-2:]
+
+ for cls in mapper.class_.__mro__:
+ if (cls, key) in self._relationship_options:
+ relationship_option = self._relationship_options[(cls, key)]
+ _set_cache_parameters(
+ query,
+ relationship_option.region,
+ relationship_option.namespace,
+ None)
+
+ def and_(self, option):
+ """Chain another RelationshipCache option to this one.
+
+ While many RelationshipCache objects can be specified on a single
+ Query separately, chaining them together allows for a more efficient
+ lookup during load.
+
+ """
+ self._relationship_options.update(option._relationship_options)
+ return self
+
+
+def _params_from_query(query):
+ """Pull the bind parameter values from a query.
+
+ This takes into account any scalar attribute bindparam set up.
+
+ E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
+ would return [5, 7].
+
+ """
+ v = []
+ def visit_bindparam(bind):
+ value = query._params.get(bind.key, bind.value)
+
+ # lazyloader may dig a callable in here, intended
+ # to late-evaluate params after autoflush is called.
+ # convert to a scalar value.
+ if callable(value):
+ value = value()
+
+ v.append(value)
+ if query._criterion is not None:
+ visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
+ return v