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