caching_query.py
289 lines
| 9.8 KiB
| text/x-python
|
PythonLexer
r547 | """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. | ||||
r1203 | * _params_from_query - extracts value parameters from | |||
r547 | a Query. | |||
The rest of what's here are standard SQLAlchemy and | ||||
Beaker constructs. | ||||
r1203 | ||||
r547 | """ | |||
r1271 | import beaker | |||
r609 | from beaker.exceptions import BeakerException | |||
r1271 | ||||
r547 | from sqlalchemy.orm.interfaces import MapperOption | |||
from sqlalchemy.orm.query import Query | ||||
from sqlalchemy.sql import visitors | ||||
r1271 | ||||
r547 | ||||
class CachingQuery(Query): | ||||
r1203 | """A Query subclass which optionally loads full results from a Beaker | |||
r547 | cache region. | |||
r1203 | ||||
r547 | The CachingQuery stores additional state that allows it to consult | |||
a Beaker cache before accessing the database: | ||||
r1203 | ||||
* A "region", which is a cache region argument passed to a | ||||
r547 | Beaker CacheManager, specifies a particular cache configuration | |||
(including backend implementation, expiration times, etc.) | ||||
* A "namespace", which is a qualifying name that identifies a | ||||
r1203 | 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 | ||||
r547 | to a joined table might use the name "related_date_range". | |||
r1203 | ||||
r547 | When the above state is present, a Beaker cache is retrieved. | |||
r1203 | ||||
The "namespace" name is first concatenated with | ||||
a string composed of the individual entities and columns the Query | ||||
r547 | requests, i.e. such as ``Query(User.id, User.name)``. | |||
r1203 | ||||
r547 | 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 | ||||
r1203 | by this query, which are usually literals defined in the | |||
r547 | WHERE clause. | |||
The FromCache and RelationshipCache mapper options below represent | ||||
the "public" method of configuring this state upon the CachingQuery. | ||||
r1046 | ||||
r547 | """ | |||
r784 | ||||
r547 | def __init__(self, manager, *args, **kw): | |||
self.cache_manager = manager | ||||
Query.__init__(self, *args, **kw) | ||||
r784 | ||||
r547 | def __iter__(self): | |||
"""override __iter__ to pull results from Beaker | ||||
if particular attributes have been configured. | ||||
r1203 | ||||
r547 | Note that this approach does *not* detach the loaded objects from | |||
the current session. If the cache backend is an in-process cache | ||||
(like "memory") and lives beyond the scope of the current session's | ||||
transaction, those objects may be expired. The method here can be | ||||
modified to first expunge() each loaded item from the current | ||||
session before returning the list of items, so that the items | ||||
in the cache are not the same ones in the current Session. | ||||
r1203 | ||||
r547 | """ | |||
if hasattr(self, '_cache_parameters'): | ||||
r1271 | return self.get_value(createfunc=lambda: | |||
list(Query.__iter__(self))) | ||||
r547 | else: | |||
return Query.__iter__(self) | ||||
def invalidate(self): | ||||
"""Invalidate the value represented by 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. | ||||
Raise KeyError if no value present and no | ||||
createfunc specified. | ||||
""" | ||||
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) | ||||
r784 | cache.put(cache_key, value) | |||
r547 | ||||
r1271 | ||||
r1339 | def query_callable(manager, query_cls=CachingQuery): | |||
r547 | def query(*arg, **kw): | |||
r1339 | return query_cls(manager, *arg, **kw) | |||
r547 | return query | |||
r609 | ||||
r1271 | ||||
r609 | def get_cache_region(name, region): | |||
r610 | if region not in beaker.cache.cache_regions: | |||
r784 | raise BeakerException('Cache region `%s` not configured ' | |||
r610 | 'Check if proper cache settings are in the .ini files' % region) | |||
kw = beaker.cache.cache_regions[region] | ||||
return beaker.cache.Cache._get_cache(name, kw) | ||||
r784 | ||||
r1271 | ||||
r547 | 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'): | ||||
r1271 | raise ValueError("This Query does not have caching " | |||
"parameters configured.") | ||||
r547 | ||||
region, namespace, cache_key = query._cache_parameters | ||||
r784 | ||||
r547 | 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 | ||||
r609 | #cache = query.cache_manager.get_cache_region(namespace, region) | |||
cache = get_cache_region(namespace, region) | ||||
r547 | # 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 | ||||
r1271 | ||||
r547 | def _namespace_from_query(namespace, query): | |||
r1203 | # cache namespace - the token handed in by the | |||
r547 | # option + class we're querying against | |||
namespace = " ".join([namespace] + [str(x) for x in query._entities]) | ||||
# memcached wants this | ||||
namespace = namespace.replace(' ', '_') | ||||
return namespace | ||||
r1271 | ||||
r547 | def _set_cache_parameters(query, region, namespace, cache_key): | |||
r784 | ||||
r547 | if hasattr(query, '_cache_parameters'): | |||
region, namespace, cache_key = query._cache_parameters | ||||
raise ValueError("This query is already configured " | ||||
r784 | "for region %r namespace %r" % | |||
r547 | (region, namespace) | |||
) | ||||
query._cache_parameters = region, namespace, cache_key | ||||
r784 | ||||
r1271 | ||||
r547 | 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. | ||||
r1203 | ||||
r547 | :param region: the cache region. Should be a | |||
region configured in the Beaker CacheManager. | ||||
r1203 | ||||
r547 | :param namespace: the cache namespace. Should | |||
be a name uniquely describing the target Query's | ||||
lexical structure. | ||||
r1203 | ||||
:param cache_key: optional. A string cache key | ||||
r547 | that will serve as the key to the query. Use this | |||
if your query has a huge amount of parameters (such | ||||
r1203 | as when using in_()) which correspond more simply to | |||
r547 | some other identifier. | |||
""" | ||||
self.region = region | ||||
self.namespace = namespace | ||||
self.cache_key = cache_key | ||||
r784 | ||||
r547 | def process_query(self, query): | |||
"""Process a Query during normal loading operation.""" | ||||
r784 | ||||
r1271 | _set_cache_parameters(query, self.region, self.namespace, | |||
self.cache_key) | ||||
r547 | ||||
class RelationshipCache(MapperOption): | ||||
r1203 | """Specifies that a Query as called within a "lazy load" | |||
r547 | should load results from a cache.""" | |||
propagate_to_loaders = True | ||||
def __init__(self, region, namespace, attribute): | ||||
"""Construct a new RelationshipCache. | ||||
r1203 | ||||
r547 | :param region: the cache region. Should be a | |||
region configured in the Beaker CacheManager. | ||||
r1203 | ||||
r547 | :param namespace: the cache namespace. Should | |||
be a name uniquely describing the target Query's | ||||
lexical structure. | ||||
r1203 | ||||
r547 | :param attribute: A Class.attribute which | |||
indicates a particular class relationship() whose | ||||
lazy loader should be pulled from the cache. | ||||
r1203 | ||||
r547 | """ | |||
self.region = region | ||||
self.namespace = namespace | ||||
self._relationship_options = { | ||||
r1271 | (attribute.property.parent.class_, attribute.property.key): self | |||
r547 | } | |||
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: | ||||
r1271 | relationship_option = \ | |||
self._relationship_options[(cls, key)] | ||||
r547 | _set_cache_parameters( | |||
query, | ||||
relationship_option.region, | ||||
relationship_option.namespace, | ||||
None) | ||||
def and_(self, option): | ||||
"""Chain another RelationshipCache option to this one. | ||||
r1203 | ||||
r547 | While many RelationshipCache objects can be specified on a single | |||
Query separately, chaining them together allows for a more efficient | ||||
lookup during load. | ||||
r1203 | ||||
r547 | """ | |||
self._relationship_options.update(option._relationship_options) | ||||
return self | ||||
def _params_from_query(query): | ||||
"""Pull the bind parameter values from a query. | ||||
r1203 | ||||
r547 | This takes into account any scalar attribute bindparam set up. | |||
r1203 | ||||
r547 | E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7))) | |||
would return [5, 7]. | ||||
r1203 | ||||
r547 | """ | |||
v = [] | ||||
def visit_bindparam(bind): | ||||
value = query._params.get(bind.key, bind.value) | ||||
r1365 | ||||
r547 | # 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() | ||||
r784 | ||||
r547 | v.append(value) | |||
if query._criterion is not None: | ||||
r1339 | visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam}) | |||
r547 | return v | |||