##// END OF EJS Templates
applied caching query update from latest sqlalchemy
marcink -
r1365:cd865113 beta
parent child Browse files
Show More
@@ -1,290 +1,289 b''
1 """caching_query.py
1 """caching_query.py
2
2
3 Represent persistence structures which allow the usage of
3 Represent persistence structures which allow the usage of
4 Beaker caching with SQLAlchemy.
4 Beaker caching with SQLAlchemy.
5
5
6 The three new concepts introduced here are:
6 The three new concepts introduced here are:
7
7
8 * CachingQuery - a Query subclass that caches and
8 * CachingQuery - a Query subclass that caches and
9 retrieves results in/from Beaker.
9 retrieves results in/from Beaker.
10 * FromCache - a query option that establishes caching
10 * FromCache - a query option that establishes caching
11 parameters on a Query
11 parameters on a Query
12 * RelationshipCache - a variant of FromCache which is specific
12 * RelationshipCache - a variant of FromCache which is specific
13 to a query invoked during a lazy load.
13 to a query invoked during a lazy load.
14 * _params_from_query - extracts value parameters from
14 * _params_from_query - extracts value parameters from
15 a Query.
15 a Query.
16
16
17 The rest of what's here are standard SQLAlchemy and
17 The rest of what's here are standard SQLAlchemy and
18 Beaker constructs.
18 Beaker constructs.
19
19
20 """
20 """
21 import beaker
21 import beaker
22 from beaker.exceptions import BeakerException
22 from beaker.exceptions import BeakerException
23
23
24 from sqlalchemy.orm.interfaces import MapperOption
24 from sqlalchemy.orm.interfaces import MapperOption
25 from sqlalchemy.orm.query import Query
25 from sqlalchemy.orm.query import Query
26 from sqlalchemy.sql import visitors
26 from sqlalchemy.sql import visitors
27
27
28
28
29 class CachingQuery(Query):
29 class CachingQuery(Query):
30 """A Query subclass which optionally loads full results from a Beaker
30 """A Query subclass which optionally loads full results from a Beaker
31 cache region.
31 cache region.
32
32
33 The CachingQuery stores additional state that allows it to consult
33 The CachingQuery stores additional state that allows it to consult
34 a Beaker cache before accessing the database:
34 a Beaker cache before accessing the database:
35
35
36 * A "region", which is a cache region argument passed to a
36 * A "region", which is a cache region argument passed to a
37 Beaker CacheManager, specifies a particular cache configuration
37 Beaker CacheManager, specifies a particular cache configuration
38 (including backend implementation, expiration times, etc.)
38 (including backend implementation, expiration times, etc.)
39 * A "namespace", which is a qualifying name that identifies a
39 * A "namespace", which is a qualifying name that identifies a
40 group of keys within the cache. A query that filters on a name
40 group of keys within the cache. A query that filters on a name
41 might use the name "by_name", a query that filters on a date range
41 might use the name "by_name", a query that filters on a date range
42 to a joined table might use the name "related_date_range".
42 to a joined table might use the name "related_date_range".
43
43
44 When the above state is present, a Beaker cache is retrieved.
44 When the above state is present, a Beaker cache is retrieved.
45
45
46 The "namespace" name is first concatenated with
46 The "namespace" name is first concatenated with
47 a string composed of the individual entities and columns the Query
47 a string composed of the individual entities and columns the Query
48 requests, i.e. such as ``Query(User.id, User.name)``.
48 requests, i.e. such as ``Query(User.id, User.name)``.
49
49
50 The Beaker cache is then loaded from the cache manager based
50 The Beaker cache is then loaded from the cache manager based
51 on the region and composed namespace. The key within the cache
51 on the region and composed namespace. The key within the cache
52 itself is then constructed against the bind parameters specified
52 itself is then constructed against the bind parameters specified
53 by this query, which are usually literals defined in the
53 by this query, which are usually literals defined in the
54 WHERE clause.
54 WHERE clause.
55
55
56 The FromCache and RelationshipCache mapper options below represent
56 The FromCache and RelationshipCache mapper options below represent
57 the "public" method of configuring this state upon the CachingQuery.
57 the "public" method of configuring this state upon the CachingQuery.
58
58
59 """
59 """
60
60
61 def __init__(self, manager, *args, **kw):
61 def __init__(self, manager, *args, **kw):
62 self.cache_manager = manager
62 self.cache_manager = manager
63 Query.__init__(self, *args, **kw)
63 Query.__init__(self, *args, **kw)
64
64
65 def __iter__(self):
65 def __iter__(self):
66 """override __iter__ to pull results from Beaker
66 """override __iter__ to pull results from Beaker
67 if particular attributes have been configured.
67 if particular attributes have been configured.
68
68
69 Note that this approach does *not* detach the loaded objects from
69 Note that this approach does *not* detach the loaded objects from
70 the current session. If the cache backend is an in-process cache
70 the current session. If the cache backend is an in-process cache
71 (like "memory") and lives beyond the scope of the current session's
71 (like "memory") and lives beyond the scope of the current session's
72 transaction, those objects may be expired. The method here can be
72 transaction, those objects may be expired. The method here can be
73 modified to first expunge() each loaded item from the current
73 modified to first expunge() each loaded item from the current
74 session before returning the list of items, so that the items
74 session before returning the list of items, so that the items
75 in the cache are not the same ones in the current Session.
75 in the cache are not the same ones in the current Session.
76
76
77 """
77 """
78 if hasattr(self, '_cache_parameters'):
78 if hasattr(self, '_cache_parameters'):
79 return self.get_value(createfunc=lambda:
79 return self.get_value(createfunc=lambda:
80 list(Query.__iter__(self)))
80 list(Query.__iter__(self)))
81 else:
81 else:
82 return Query.__iter__(self)
82 return Query.__iter__(self)
83
83
84 def invalidate(self):
84 def invalidate(self):
85 """Invalidate the value represented by this Query."""
85 """Invalidate the value represented by this Query."""
86
86
87 cache, cache_key = _get_cache_parameters(self)
87 cache, cache_key = _get_cache_parameters(self)
88 cache.remove(cache_key)
88 cache.remove(cache_key)
89
89
90 def get_value(self, merge=True, createfunc=None):
90 def get_value(self, merge=True, createfunc=None):
91 """Return the value from the cache for this query.
91 """Return the value from the cache for this query.
92
92
93 Raise KeyError if no value present and no
93 Raise KeyError if no value present and no
94 createfunc specified.
94 createfunc specified.
95
95
96 """
96 """
97 cache, cache_key = _get_cache_parameters(self)
97 cache, cache_key = _get_cache_parameters(self)
98 ret = cache.get_value(cache_key, createfunc=createfunc)
98 ret = cache.get_value(cache_key, createfunc=createfunc)
99 if merge:
99 if merge:
100 ret = self.merge_result(ret, load=False)
100 ret = self.merge_result(ret, load=False)
101 return ret
101 return ret
102
102
103 def set_value(self, value):
103 def set_value(self, value):
104 """Set the value in the cache for this query."""
104 """Set the value in the cache for this query."""
105
105
106 cache, cache_key = _get_cache_parameters(self)
106 cache, cache_key = _get_cache_parameters(self)
107 cache.put(cache_key, value)
107 cache.put(cache_key, value)
108
108
109
109
110 def query_callable(manager, query_cls=CachingQuery):
110 def query_callable(manager, query_cls=CachingQuery):
111 def query(*arg, **kw):
111 def query(*arg, **kw):
112 return query_cls(manager, *arg, **kw)
112 return query_cls(manager, *arg, **kw)
113 return query
113 return query
114
114
115
115
116 def get_cache_region(name, region):
116 def get_cache_region(name, region):
117 if region not in beaker.cache.cache_regions:
117 if region not in beaker.cache.cache_regions:
118 raise BeakerException('Cache region `%s` not configured '
118 raise BeakerException('Cache region `%s` not configured '
119 'Check if proper cache settings are in the .ini files' % region)
119 'Check if proper cache settings are in the .ini files' % region)
120 kw = beaker.cache.cache_regions[region]
120 kw = beaker.cache.cache_regions[region]
121 return beaker.cache.Cache._get_cache(name, kw)
121 return beaker.cache.Cache._get_cache(name, kw)
122
122
123
123
124 def _get_cache_parameters(query):
124 def _get_cache_parameters(query):
125 """For a query with cache_region and cache_namespace configured,
125 """For a query with cache_region and cache_namespace configured,
126 return the correspoinding Cache instance and cache key, based
126 return the correspoinding Cache instance and cache key, based
127 on this query's current criterion and parameter values.
127 on this query's current criterion and parameter values.
128
128
129 """
129 """
130 if not hasattr(query, '_cache_parameters'):
130 if not hasattr(query, '_cache_parameters'):
131 raise ValueError("This Query does not have caching "
131 raise ValueError("This Query does not have caching "
132 "parameters configured.")
132 "parameters configured.")
133
133
134 region, namespace, cache_key = query._cache_parameters
134 region, namespace, cache_key = query._cache_parameters
135
135
136 namespace = _namespace_from_query(namespace, query)
136 namespace = _namespace_from_query(namespace, query)
137
137
138 if cache_key is None:
138 if cache_key is None:
139 # cache key - the value arguments from this query's parameters.
139 # cache key - the value arguments from this query's parameters.
140 args = _params_from_query(query)
140 args = _params_from_query(query)
141 cache_key = " ".join([str(x) for x in args])
141 cache_key = " ".join([str(x) for x in args])
142
142
143 # get cache
143 # get cache
144 #cache = query.cache_manager.get_cache_region(namespace, region)
144 #cache = query.cache_manager.get_cache_region(namespace, region)
145 cache = get_cache_region(namespace, region)
145 cache = get_cache_region(namespace, region)
146 # optional - hash the cache_key too for consistent length
146 # optional - hash the cache_key too for consistent length
147 # import uuid
147 # import uuid
148 # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key))
148 # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key))
149
149
150 return cache, cache_key
150 return cache, cache_key
151
151
152
152
153 def _namespace_from_query(namespace, query):
153 def _namespace_from_query(namespace, query):
154 # cache namespace - the token handed in by the
154 # cache namespace - the token handed in by the
155 # option + class we're querying against
155 # option + class we're querying against
156 namespace = " ".join([namespace] + [str(x) for x in query._entities])
156 namespace = " ".join([namespace] + [str(x) for x in query._entities])
157
157
158 # memcached wants this
158 # memcached wants this
159 namespace = namespace.replace(' ', '_')
159 namespace = namespace.replace(' ', '_')
160
160
161 return namespace
161 return namespace
162
162
163
163
164 def _set_cache_parameters(query, region, namespace, cache_key):
164 def _set_cache_parameters(query, region, namespace, cache_key):
165
165
166 if hasattr(query, '_cache_parameters'):
166 if hasattr(query, '_cache_parameters'):
167 region, namespace, cache_key = query._cache_parameters
167 region, namespace, cache_key = query._cache_parameters
168 raise ValueError("This query is already configured "
168 raise ValueError("This query is already configured "
169 "for region %r namespace %r" %
169 "for region %r namespace %r" %
170 (region, namespace)
170 (region, namespace)
171 )
171 )
172 query._cache_parameters = region, namespace, cache_key
172 query._cache_parameters = region, namespace, cache_key
173
173
174
174
175 class FromCache(MapperOption):
175 class FromCache(MapperOption):
176 """Specifies that a Query should load results from a cache."""
176 """Specifies that a Query should load results from a cache."""
177
177
178 propagate_to_loaders = False
178 propagate_to_loaders = False
179
179
180 def __init__(self, region, namespace, cache_key=None):
180 def __init__(self, region, namespace, cache_key=None):
181 """Construct a new FromCache.
181 """Construct a new FromCache.
182
182
183 :param region: the cache region. Should be a
183 :param region: the cache region. Should be a
184 region configured in the Beaker CacheManager.
184 region configured in the Beaker CacheManager.
185
185
186 :param namespace: the cache namespace. Should
186 :param namespace: the cache namespace. Should
187 be a name uniquely describing the target Query's
187 be a name uniquely describing the target Query's
188 lexical structure.
188 lexical structure.
189
189
190 :param cache_key: optional. A string cache key
190 :param cache_key: optional. A string cache key
191 that will serve as the key to the query. Use this
191 that will serve as the key to the query. Use this
192 if your query has a huge amount of parameters (such
192 if your query has a huge amount of parameters (such
193 as when using in_()) which correspond more simply to
193 as when using in_()) which correspond more simply to
194 some other identifier.
194 some other identifier.
195
195
196 """
196 """
197 self.region = region
197 self.region = region
198 self.namespace = namespace
198 self.namespace = namespace
199 self.cache_key = cache_key
199 self.cache_key = cache_key
200
200
201 def process_query(self, query):
201 def process_query(self, query):
202 """Process a Query during normal loading operation."""
202 """Process a Query during normal loading operation."""
203
203
204 _set_cache_parameters(query, self.region, self.namespace,
204 _set_cache_parameters(query, self.region, self.namespace,
205 self.cache_key)
205 self.cache_key)
206
206
207
207
208 class RelationshipCache(MapperOption):
208 class RelationshipCache(MapperOption):
209 """Specifies that a Query as called within a "lazy load"
209 """Specifies that a Query as called within a "lazy load"
210 should load results from a cache."""
210 should load results from a cache."""
211
211
212 propagate_to_loaders = True
212 propagate_to_loaders = True
213
213
214 def __init__(self, region, namespace, attribute):
214 def __init__(self, region, namespace, attribute):
215 """Construct a new RelationshipCache.
215 """Construct a new RelationshipCache.
216
216
217 :param region: the cache region. Should be a
217 :param region: the cache region. Should be a
218 region configured in the Beaker CacheManager.
218 region configured in the Beaker CacheManager.
219
219
220 :param namespace: the cache namespace. Should
220 :param namespace: the cache namespace. Should
221 be a name uniquely describing the target Query's
221 be a name uniquely describing the target Query's
222 lexical structure.
222 lexical structure.
223
223
224 :param attribute: A Class.attribute which
224 :param attribute: A Class.attribute which
225 indicates a particular class relationship() whose
225 indicates a particular class relationship() whose
226 lazy loader should be pulled from the cache.
226 lazy loader should be pulled from the cache.
227
227
228 """
228 """
229 self.region = region
229 self.region = region
230 self.namespace = namespace
230 self.namespace = namespace
231 self._relationship_options = {
231 self._relationship_options = {
232 (attribute.property.parent.class_, attribute.property.key): self
232 (attribute.property.parent.class_, attribute.property.key): self
233 }
233 }
234
234
235 def process_query_conditionally(self, query):
235 def process_query_conditionally(self, query):
236 """Process a Query that is used within a lazy loader.
236 """Process a Query that is used within a lazy loader.
237
237
238 (the process_query_conditionally() method is a SQLAlchemy
238 (the process_query_conditionally() method is a SQLAlchemy
239 hook invoked only within lazyload.)
239 hook invoked only within lazyload.)
240
240
241 """
241 """
242 if query._current_path:
242 if query._current_path:
243 mapper, key = query._current_path[-2:]
243 mapper, key = query._current_path[-2:]
244
244
245 for cls in mapper.class_.__mro__:
245 for cls in mapper.class_.__mro__:
246 if (cls, key) in self._relationship_options:
246 if (cls, key) in self._relationship_options:
247 relationship_option = \
247 relationship_option = \
248 self._relationship_options[(cls, key)]
248 self._relationship_options[(cls, key)]
249 _set_cache_parameters(
249 _set_cache_parameters(
250 query,
250 query,
251 relationship_option.region,
251 relationship_option.region,
252 relationship_option.namespace,
252 relationship_option.namespace,
253 None)
253 None)
254
254
255 def and_(self, option):
255 def and_(self, option):
256 """Chain another RelationshipCache option to this one.
256 """Chain another RelationshipCache option to this one.
257
257
258 While many RelationshipCache objects can be specified on a single
258 While many RelationshipCache objects can be specified on a single
259 Query separately, chaining them together allows for a more efficient
259 Query separately, chaining them together allows for a more efficient
260 lookup during load.
260 lookup during load.
261
261
262 """
262 """
263 self._relationship_options.update(option._relationship_options)
263 self._relationship_options.update(option._relationship_options)
264 return self
264 return self
265
265
266
266
267 def _params_from_query(query):
267 def _params_from_query(query):
268 """Pull the bind parameter values from a query.
268 """Pull the bind parameter values from a query.
269
269
270 This takes into account any scalar attribute bindparam set up.
270 This takes into account any scalar attribute bindparam set up.
271
271
272 E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
272 E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
273 would return [5, 7].
273 would return [5, 7].
274
274
275 """
275 """
276 v = []
276 v = []
277 def visit_bindparam(bind):
277 def visit_bindparam(bind):
278 value = query._params.get(bind.key, bind.value)
278 value = query._params.get(bind.key, bind.value)
279
279 # lazyloader may dig a callable in here, intended
280 # lazyloader may dig a callable in here, intended
280 # to late-evaluate params after autoflush is called.
281 # to late-evaluate params after autoflush is called.
281 # convert to a scalar value.
282 # convert to a scalar value.
282 if callable(value):
283 if callable(value):
283 value = value()
284 value = value()
284
285
285 v.append(value)
286 v.append(value)
286 if query._criterion is not None:
287 if query._criterion is not None:
287 visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
288 visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
288 for f in query._from_obj:
289 visitors.traverse(f, {}, {'bindparam':visit_bindparam})
290 return v
289 return v
General Comments 0
You need to be logged in to leave comments. Login now