1 | # strategies.py |
---|
2 | # Copyright (C) 2005, 2006, 2007, 2008, 2009 Michael Bayer mike_mp@zzzcomputing.com |
---|
3 | # |
---|
4 | # This module is part of SQLAlchemy and is released under |
---|
5 | # the MIT License: http://www.opensource.org/licenses/mit-license.php |
---|
6 | |
---|
7 | """sqlalchemy.orm.interfaces.LoaderStrategy implementations, and related MapperOptions.""" |
---|
8 | |
---|
9 | import sqlalchemy.exceptions as sa_exc |
---|
10 | from sqlalchemy import sql, util, log |
---|
11 | from sqlalchemy.sql import util as sql_util |
---|
12 | from sqlalchemy.sql import visitors, expression, operators |
---|
13 | from sqlalchemy.orm import mapper, attributes, interfaces |
---|
14 | from sqlalchemy.orm.interfaces import ( |
---|
15 | LoaderStrategy, StrategizedOption, MapperOption, PropertyOption, |
---|
16 | serialize_path, deserialize_path, StrategizedProperty |
---|
17 | ) |
---|
18 | from sqlalchemy.orm import session as sessionlib |
---|
19 | from sqlalchemy.orm import util as mapperutil |
---|
20 | |
---|
21 | def _register_attribute(strategy, mapper, useobject, |
---|
22 | compare_function=None, |
---|
23 | typecallable=None, |
---|
24 | copy_function=None, |
---|
25 | mutable_scalars=False, |
---|
26 | uselist=False, |
---|
27 | callable_=None, |
---|
28 | proxy_property=None, |
---|
29 | active_history=False, |
---|
30 | impl_class=None, |
---|
31 | **kw |
---|
32 | ): |
---|
33 | |
---|
34 | prop = strategy.parent_property |
---|
35 | attribute_ext = list(util.to_list(prop.extension, default=[])) |
---|
36 | |
---|
37 | if useobject and prop.single_parent: |
---|
38 | attribute_ext.append(_SingleParentValidator(prop)) |
---|
39 | |
---|
40 | if getattr(prop, 'backref', None): |
---|
41 | attribute_ext.append(prop.backref.extension) |
---|
42 | |
---|
43 | if prop.key in prop.parent._validators: |
---|
44 | attribute_ext.append(mapperutil.Validator(prop.key, prop.parent._validators[prop.key])) |
---|
45 | |
---|
46 | if useobject: |
---|
47 | attribute_ext.append(sessionlib.UOWEventHandler(prop.key)) |
---|
48 | |
---|
49 | for m in mapper.polymorphic_iterator(): |
---|
50 | if prop is m._props.get(prop.key): |
---|
51 | |
---|
52 | attributes.register_attribute_impl( |
---|
53 | m.class_, |
---|
54 | prop.key, |
---|
55 | parent_token=prop, |
---|
56 | mutable_scalars=mutable_scalars, |
---|
57 | uselist=uselist, |
---|
58 | copy_function=copy_function, |
---|
59 | compare_function=compare_function, |
---|
60 | useobject=useobject, |
---|
61 | extension=attribute_ext, |
---|
62 | trackparent=useobject, |
---|
63 | typecallable=typecallable, |
---|
64 | callable_=callable_, |
---|
65 | active_history=active_history, |
---|
66 | impl_class=impl_class, |
---|
67 | **kw |
---|
68 | ) |
---|
69 | |
---|
70 | class UninstrumentedColumnLoader(LoaderStrategy): |
---|
71 | """Represent the strategy for a MapperProperty that doesn't instrument the class. |
---|
72 | |
---|
73 | The polymorphic_on argument of mapper() often results in this, |
---|
74 | if the argument is against the with_polymorphic selectable. |
---|
75 | |
---|
76 | """ |
---|
77 | def init(self): |
---|
78 | self.columns = self.parent_property.columns |
---|
79 | |
---|
80 | def setup_query(self, context, entity, path, adapter, column_collection=None, **kwargs): |
---|
81 | for c in self.columns: |
---|
82 | if adapter: |
---|
83 | c = adapter.columns[c] |
---|
84 | column_collection.append(c) |
---|
85 | |
---|
86 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
87 | return (None, None) |
---|
88 | |
---|
89 | class ColumnLoader(LoaderStrategy): |
---|
90 | """Strategize the loading of a plain column-based MapperProperty.""" |
---|
91 | |
---|
92 | def init(self): |
---|
93 | self.columns = self.parent_property.columns |
---|
94 | self.is_composite = hasattr(self.parent_property, 'composite_class') |
---|
95 | |
---|
96 | def setup_query(self, context, entity, path, adapter, column_collection=None, **kwargs): |
---|
97 | for c in self.columns: |
---|
98 | if adapter: |
---|
99 | c = adapter.columns[c] |
---|
100 | column_collection.append(c) |
---|
101 | |
---|
102 | def init_class_attribute(self, mapper): |
---|
103 | self.is_class_level = True |
---|
104 | coltype = self.columns[0].type |
---|
105 | active_history = self.columns[0].primary_key # TODO: check all columns ? check for foreign Key as well? |
---|
106 | |
---|
107 | _register_attribute(self, mapper, useobject=False, |
---|
108 | compare_function=coltype.compare_values, |
---|
109 | copy_function=coltype.copy_value, |
---|
110 | mutable_scalars=self.columns[0].type.is_mutable(), |
---|
111 | active_history = active_history |
---|
112 | ) |
---|
113 | |
---|
114 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
115 | key, col = self.key, self.columns[0] |
---|
116 | if adapter: |
---|
117 | col = adapter.columns[col] |
---|
118 | if col is not None and col in row: |
---|
119 | def new_execute(state, dict_, row, **flags): |
---|
120 | dict_[key] = row[col] |
---|
121 | |
---|
122 | if self._should_log_debug: |
---|
123 | new_execute = self.debug_callable(new_execute, self.logger, |
---|
124 | "%s returning active column fetcher" % self, |
---|
125 | lambda state, dict_, row, **flags: "%s populating %s" % \ |
---|
126 | (self, |
---|
127 | mapperutil.state_attribute_str(state, key)) |
---|
128 | ) |
---|
129 | return (new_execute, None) |
---|
130 | else: |
---|
131 | def new_execute(state, dict_, row, isnew, **flags): |
---|
132 | if isnew: |
---|
133 | state.expire_attributes([key]) |
---|
134 | if self._should_log_debug: |
---|
135 | self.logger.debug("%s deferring load" % self) |
---|
136 | return (new_execute, None) |
---|
137 | |
---|
138 | log.class_logger(ColumnLoader) |
---|
139 | |
---|
140 | class CompositeColumnLoader(ColumnLoader): |
---|
141 | """Strategize the loading of a composite column-based MapperProperty.""" |
---|
142 | |
---|
143 | def init_class_attribute(self, mapper): |
---|
144 | self.is_class_level = True |
---|
145 | self.logger.info("%s register managed composite attribute" % self) |
---|
146 | |
---|
147 | def copy(obj): |
---|
148 | if obj is None: |
---|
149 | return None |
---|
150 | return self.parent_property.composite_class(*obj.__composite_values__()) |
---|
151 | |
---|
152 | def compare(a, b): |
---|
153 | if a is None or b is None: |
---|
154 | return a is b |
---|
155 | |
---|
156 | for col, aprop, bprop in zip(self.columns, |
---|
157 | a.__composite_values__(), |
---|
158 | b.__composite_values__()): |
---|
159 | if not col.type.compare_values(aprop, bprop): |
---|
160 | return False |
---|
161 | else: |
---|
162 | return True |
---|
163 | |
---|
164 | _register_attribute(self, mapper, useobject=False, |
---|
165 | compare_function=compare, |
---|
166 | copy_function=copy, |
---|
167 | mutable_scalars=True |
---|
168 | #active_history ? |
---|
169 | ) |
---|
170 | |
---|
171 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
172 | key, columns, composite_class = self.key, self.columns, self.parent_property.composite_class |
---|
173 | if adapter: |
---|
174 | columns = [adapter.columns[c] for c in columns] |
---|
175 | for c in columns: |
---|
176 | if c not in row: |
---|
177 | def new_execute(state, dict_, row, isnew, **flags): |
---|
178 | if isnew: |
---|
179 | state.expire_attributes([key]) |
---|
180 | if self._should_log_debug: |
---|
181 | self.logger.debug("%s deferring load" % self) |
---|
182 | return (new_execute, None) |
---|
183 | else: |
---|
184 | def new_execute(state, dict_, row, **flags): |
---|
185 | dict_[key] = composite_class(*[row[c] for c in columns]) |
---|
186 | |
---|
187 | if self._should_log_debug: |
---|
188 | new_execute = self.debug_callable(new_execute, self.logger, |
---|
189 | "%s returning active composite column fetcher" % self, |
---|
190 | lambda state, dict_, row, **flags: "populating %s" % \ |
---|
191 | (mapperutil.state_attribute_str(state, key)) |
---|
192 | ) |
---|
193 | |
---|
194 | return (new_execute, None) |
---|
195 | |
---|
196 | log.class_logger(CompositeColumnLoader) |
---|
197 | |
---|
198 | class DeferredColumnLoader(LoaderStrategy): |
---|
199 | """Strategize the loading of a deferred column-based MapperProperty.""" |
---|
200 | |
---|
201 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
202 | col = self.columns[0] |
---|
203 | if adapter: |
---|
204 | col = adapter.columns[col] |
---|
205 | if col in row: |
---|
206 | return self.parent_property._get_strategy(ColumnLoader).create_row_processor(selectcontext, path, mapper, row, adapter) |
---|
207 | |
---|
208 | elif not self.is_class_level: |
---|
209 | def new_execute(state, dict_, row, **flags): |
---|
210 | state.set_callable(self.key, LoadDeferredColumns(state, self.key)) |
---|
211 | else: |
---|
212 | def new_execute(state, dict_, row, **flags): |
---|
213 | # reset state on the key so that deferred callables |
---|
214 | # fire off on next access. |
---|
215 | state.reset(self.key, dict_) |
---|
216 | |
---|
217 | if self._should_log_debug: |
---|
218 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
219 | lambda state, dict_, row, **flags: "set deferred callable on %s" % \ |
---|
220 | mapperutil.state_attribute_str(state, self.key) |
---|
221 | ) |
---|
222 | return (new_execute, None) |
---|
223 | |
---|
224 | def init(self): |
---|
225 | if hasattr(self.parent_property, 'composite_class'): |
---|
226 | raise NotImplementedError("Deferred loading for composite types not implemented yet") |
---|
227 | self.columns = self.parent_property.columns |
---|
228 | self.group = self.parent_property.group |
---|
229 | |
---|
230 | def init_class_attribute(self, mapper): |
---|
231 | self.is_class_level = True |
---|
232 | |
---|
233 | _register_attribute(self, mapper, useobject=False, |
---|
234 | compare_function=self.columns[0].type.compare_values, |
---|
235 | copy_function=self.columns[0].type.copy_value, |
---|
236 | mutable_scalars=self.columns[0].type.is_mutable(), |
---|
237 | callable_=self._class_level_loader, |
---|
238 | dont_expire_missing=True |
---|
239 | ) |
---|
240 | |
---|
241 | def setup_query(self, context, entity, path, adapter, only_load_props=None, **kwargs): |
---|
242 | if \ |
---|
243 | (self.group is not None and context.attributes.get(('undefer', self.group), False)) or \ |
---|
244 | (only_load_props and self.key in only_load_props): |
---|
245 | |
---|
246 | self.parent_property._get_strategy(ColumnLoader).setup_query(context, entity, path, adapter, **kwargs) |
---|
247 | |
---|
248 | def _class_level_loader(self, state): |
---|
249 | if not mapperutil._state_has_identity(state): |
---|
250 | return None |
---|
251 | |
---|
252 | return LoadDeferredColumns(state, self.key) |
---|
253 | |
---|
254 | |
---|
255 | log.class_logger(DeferredColumnLoader) |
---|
256 | |
---|
257 | class LoadDeferredColumns(object): |
---|
258 | """serializable loader object used by DeferredColumnLoader""" |
---|
259 | |
---|
260 | def __init__(self, state, key): |
---|
261 | self.state, self.key = state, key |
---|
262 | |
---|
263 | def __call__(self): |
---|
264 | state = self.state |
---|
265 | |
---|
266 | |
---|
267 | localparent = mapper._state_mapper(state) |
---|
268 | |
---|
269 | prop = localparent.get_property(self.key) |
---|
270 | strategy = prop._get_strategy(DeferredColumnLoader) |
---|
271 | |
---|
272 | if strategy.group: |
---|
273 | toload = [ |
---|
274 | p.key for p in |
---|
275 | localparent.iterate_properties |
---|
276 | if isinstance(p, StrategizedProperty) and |
---|
277 | isinstance(p.strategy, DeferredColumnLoader) and |
---|
278 | p.group==strategy.group |
---|
279 | ] |
---|
280 | else: |
---|
281 | toload = [self.key] |
---|
282 | |
---|
283 | # narrow the keys down to just those which have no history |
---|
284 | group = [k for k in toload if k in state.unmodified] |
---|
285 | |
---|
286 | if strategy._should_log_debug: |
---|
287 | strategy.logger.debug( |
---|
288 | "deferred load %s group %s" % |
---|
289 | (mapperutil.state_attribute_str(state, self.key), group and ','.join(group) or 'None') |
---|
290 | ) |
---|
291 | |
---|
292 | session = sessionlib._state_session(state) |
---|
293 | if session is None: |
---|
294 | raise sa_exc.UnboundExecutionError( |
---|
295 | "Parent instance %s is not bound to a Session; " |
---|
296 | "deferred load operation of attribute '%s' cannot proceed" % |
---|
297 | (mapperutil.state_str(state), self.key) |
---|
298 | ) |
---|
299 | |
---|
300 | query = session.query(localparent) |
---|
301 | ident = state.key[1] |
---|
302 | query._get(None, ident=ident, only_load_props=group, refresh_state=state) |
---|
303 | return attributes.ATTR_WAS_SET |
---|
304 | |
---|
305 | class DeferredOption(StrategizedOption): |
---|
306 | propagate_to_loaders = True |
---|
307 | |
---|
308 | def __init__(self, key, defer=False): |
---|
309 | super(DeferredOption, self).__init__(key) |
---|
310 | self.defer = defer |
---|
311 | |
---|
312 | def get_strategy_class(self): |
---|
313 | if self.defer: |
---|
314 | return DeferredColumnLoader |
---|
315 | else: |
---|
316 | return ColumnLoader |
---|
317 | |
---|
318 | class UndeferGroupOption(MapperOption): |
---|
319 | propagate_to_loaders = True |
---|
320 | |
---|
321 | def __init__(self, group): |
---|
322 | self.group = group |
---|
323 | def process_query(self, query): |
---|
324 | query._attributes[('undefer', self.group)] = True |
---|
325 | |
---|
326 | class AbstractRelationLoader(LoaderStrategy): |
---|
327 | """LoaderStratgies which deal with related objects as opposed to scalars.""" |
---|
328 | |
---|
329 | def init(self): |
---|
330 | for attr in ['mapper', 'target', 'table', 'uselist']: |
---|
331 | setattr(self, attr, getattr(self.parent_property, attr)) |
---|
332 | |
---|
333 | def _init_instance_attribute(self, state, callable_=None): |
---|
334 | if callable_: |
---|
335 | state.set_callable(self.key, callable_) |
---|
336 | else: |
---|
337 | state.initialize(self.key) |
---|
338 | |
---|
339 | class NoLoader(AbstractRelationLoader): |
---|
340 | """Strategize a relation() that doesn't load data automatically.""" |
---|
341 | |
---|
342 | def init_class_attribute(self, mapper): |
---|
343 | self.is_class_level = True |
---|
344 | |
---|
345 | _register_attribute(self, mapper, |
---|
346 | useobject=True, |
---|
347 | uselist=self.parent_property.uselist, |
---|
348 | typecallable = self.parent_property.collection_class, |
---|
349 | ) |
---|
350 | |
---|
351 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
352 | def new_execute(state, dict_, row, **flags): |
---|
353 | self._init_instance_attribute(state) |
---|
354 | |
---|
355 | if self._should_log_debug: |
---|
356 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
357 | lambda state, dict_, row, **flags: "initializing blank scalar/collection on %s" % \ |
---|
358 | mapperutil.state_attribute_str(state, self.key) |
---|
359 | ) |
---|
360 | return (new_execute, None) |
---|
361 | |
---|
362 | log.class_logger(NoLoader) |
---|
363 | |
---|
364 | class LazyLoader(AbstractRelationLoader): |
---|
365 | """Strategize a relation() that loads when first accessed.""" |
---|
366 | |
---|
367 | def init(self): |
---|
368 | super(LazyLoader, self).init() |
---|
369 | (self.__lazywhere, self.__bind_to_col, self._equated_columns) = self._create_lazy_clause(self.parent_property) |
---|
370 | |
---|
371 | self.logger.info("%s lazy loading clause %s" % (self, self.__lazywhere)) |
---|
372 | |
---|
373 | # determine if our "lazywhere" clause is the same as the mapper's |
---|
374 | # get() clause. then we can just use mapper.get() |
---|
375 | #from sqlalchemy.orm import query |
---|
376 | self.use_get = not self.uselist and self.mapper._get_clause[0].compare(self.__lazywhere) |
---|
377 | if self.use_get: |
---|
378 | self.logger.info("%s will use query.get() to optimize instance loads" % self) |
---|
379 | |
---|
380 | def init_class_attribute(self, mapper): |
---|
381 | self.is_class_level = True |
---|
382 | |
---|
383 | # MANYTOONE currently only needs the "old" value for delete-orphan |
---|
384 | # cascades. the required _SingleParentValidator will enable active_history |
---|
385 | # in that case. otherwise we don't need the "old" value during backref operations. |
---|
386 | _register_attribute(self, |
---|
387 | mapper, |
---|
388 | useobject=True, |
---|
389 | callable_=self._class_level_loader, |
---|
390 | uselist = self.parent_property.uselist, |
---|
391 | typecallable = self.parent_property.collection_class, |
---|
392 | active_history = self.parent_property.direction is not interfaces.MANYTOONE, |
---|
393 | ) |
---|
394 | |
---|
395 | def lazy_clause(self, state, reverse_direction=False, alias_secondary=False, adapt_source=None): |
---|
396 | if state is None: |
---|
397 | return self._lazy_none_clause(reverse_direction, adapt_source=adapt_source) |
---|
398 | |
---|
399 | if not reverse_direction: |
---|
400 | (criterion, bind_to_col, rev) = (self.__lazywhere, self.__bind_to_col, self._equated_columns) |
---|
401 | else: |
---|
402 | (criterion, bind_to_col, rev) = LazyLoader._create_lazy_clause(self.parent_property, reverse_direction=reverse_direction) |
---|
403 | |
---|
404 | def visit_bindparam(bindparam): |
---|
405 | mapper = reverse_direction and self.parent_property.mapper or self.parent_property.parent |
---|
406 | if bindparam.key in bind_to_col: |
---|
407 | # use the "committed" (database) version to get query column values |
---|
408 | # also its a deferred value; so that when used by Query, the committed value is used |
---|
409 | # after an autoflush occurs |
---|
410 | o = state.obj() # strong ref |
---|
411 | bindparam.value = lambda: mapper._get_committed_attr_by_column(o, bind_to_col[bindparam.key]) |
---|
412 | |
---|
413 | if self.parent_property.secondary and alias_secondary: |
---|
414 | criterion = sql_util.ClauseAdapter(self.parent_property.secondary.alias()).traverse(criterion) |
---|
415 | |
---|
416 | criterion = visitors.cloned_traverse(criterion, {}, {'bindparam':visit_bindparam}) |
---|
417 | if adapt_source: |
---|
418 | criterion = adapt_source(criterion) |
---|
419 | return criterion |
---|
420 | |
---|
421 | def _lazy_none_clause(self, reverse_direction=False, adapt_source=None): |
---|
422 | if not reverse_direction: |
---|
423 | (criterion, bind_to_col, rev) = (self.__lazywhere, self.__bind_to_col, self._equated_columns) |
---|
424 | else: |
---|
425 | (criterion, bind_to_col, rev) = LazyLoader._create_lazy_clause(self.parent_property, reverse_direction=reverse_direction) |
---|
426 | |
---|
427 | def visit_binary(binary): |
---|
428 | mapper = reverse_direction and self.parent_property.mapper or self.parent_property.parent |
---|
429 | if isinstance(binary.left, expression._BindParamClause) and binary.left.key in bind_to_col: |
---|
430 | # reverse order if the NULL is on the left side |
---|
431 | binary.left = binary.right |
---|
432 | binary.right = expression.null() |
---|
433 | binary.operator = operators.is_ |
---|
434 | binary.negate = operators.isnot |
---|
435 | elif isinstance(binary.right, expression._BindParamClause) and binary.right.key in bind_to_col: |
---|
436 | binary.right = expression.null() |
---|
437 | binary.operator = operators.is_ |
---|
438 | binary.negate = operators.isnot |
---|
439 | |
---|
440 | criterion = visitors.cloned_traverse(criterion, {}, {'binary':visit_binary}) |
---|
441 | if adapt_source: |
---|
442 | criterion = adapt_source(criterion) |
---|
443 | return criterion |
---|
444 | |
---|
445 | def _class_level_loader(self, state): |
---|
446 | if not mapperutil._state_has_identity(state): |
---|
447 | return None |
---|
448 | |
---|
449 | return LoadLazyAttribute(state, self.key) |
---|
450 | |
---|
451 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
452 | if not self.is_class_level: |
---|
453 | def new_execute(state, dict_, row, **flags): |
---|
454 | # we are not the primary manager for this attribute on this class - set up a |
---|
455 | # per-instance lazyloader, which will override the class-level behavior. |
---|
456 | # this currently only happens when using a "lazyload" option on a "no load" |
---|
457 | # attribute - "eager" attributes always have a class-level lazyloader |
---|
458 | # installed. |
---|
459 | self._init_instance_attribute(state, callable_=LoadLazyAttribute(state, self.key)) |
---|
460 | |
---|
461 | if self._should_log_debug: |
---|
462 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
463 | lambda state, dict_, row, **flags: "set instance-level lazy loader on %s" % \ |
---|
464 | mapperutil.state_attribute_str(state, |
---|
465 | self.key) |
---|
466 | ) |
---|
467 | |
---|
468 | return (new_execute, None) |
---|
469 | else: |
---|
470 | def new_execute(state, dict_, row, **flags): |
---|
471 | # we are the primary manager for this attribute on this class - reset its |
---|
472 | # per-instance attribute state, so that the class-level lazy loader is |
---|
473 | # executed when next referenced on this instance. this is needed in |
---|
474 | # populate_existing() types of scenarios to reset any existing state. |
---|
475 | state.reset(self.key, dict_) |
---|
476 | |
---|
477 | if self._should_log_debug: |
---|
478 | new_execute = self.debug_callable(new_execute, self.logger, None, |
---|
479 | lambda state, dict_, row, **flags: "set class-level lazy loader on %s" % \ |
---|
480 | mapperutil.state_attribute_str(state, |
---|
481 | self.key) |
---|
482 | ) |
---|
483 | |
---|
484 | return (new_execute, None) |
---|
485 | |
---|
486 | def _create_lazy_clause(cls, prop, reverse_direction=False): |
---|
487 | binds = util.column_dict() |
---|
488 | lookup = util.column_dict() |
---|
489 | equated_columns = util.column_dict() |
---|
490 | |
---|
491 | if reverse_direction and not prop.secondaryjoin: |
---|
492 | for l, r in prop.local_remote_pairs: |
---|
493 | _list = lookup.setdefault(r, []) |
---|
494 | _list.append((r, l)) |
---|
495 | equated_columns[l] = r |
---|
496 | else: |
---|
497 | for l, r in prop.local_remote_pairs: |
---|
498 | _list = lookup.setdefault(l, []) |
---|
499 | _list.append((l, r)) |
---|
500 | equated_columns[r] = l |
---|
501 | |
---|
502 | def col_to_bind(col): |
---|
503 | if col in lookup: |
---|
504 | for tobind, equated in lookup[col]: |
---|
505 | if equated in binds: |
---|
506 | return None |
---|
507 | if col not in binds: |
---|
508 | binds[col] = sql.bindparam(None, None, type_=col.type) |
---|
509 | return binds[col] |
---|
510 | return None |
---|
511 | |
---|
512 | lazywhere = prop.primaryjoin |
---|
513 | |
---|
514 | if not prop.secondaryjoin or not reverse_direction: |
---|
515 | lazywhere = visitors.replacement_traverse(lazywhere, {}, col_to_bind) |
---|
516 | |
---|
517 | if prop.secondaryjoin is not None: |
---|
518 | secondaryjoin = prop.secondaryjoin |
---|
519 | if reverse_direction: |
---|
520 | secondaryjoin = visitors.replacement_traverse(secondaryjoin, {}, col_to_bind) |
---|
521 | lazywhere = sql.and_(lazywhere, secondaryjoin) |
---|
522 | |
---|
523 | bind_to_col = dict((binds[col].key, col) for col in binds) |
---|
524 | |
---|
525 | return (lazywhere, bind_to_col, equated_columns) |
---|
526 | _create_lazy_clause = classmethod(_create_lazy_clause) |
---|
527 | |
---|
528 | log.class_logger(LazyLoader) |
---|
529 | |
---|
530 | class LoadLazyAttribute(object): |
---|
531 | """serializable loader object used by LazyLoader""" |
---|
532 | |
---|
533 | def __init__(self, state, key): |
---|
534 | self.state, self.key = state, key |
---|
535 | |
---|
536 | def __getstate__(self): |
---|
537 | return (self.state, self.key) |
---|
538 | |
---|
539 | def __setstate__(self, state): |
---|
540 | self.state, self.key = state |
---|
541 | |
---|
542 | def __call__(self): |
---|
543 | state = self.state |
---|
544 | |
---|
545 | instance_mapper = mapper._state_mapper(state) |
---|
546 | prop = instance_mapper.get_property(self.key) |
---|
547 | strategy = prop._get_strategy(LazyLoader) |
---|
548 | |
---|
549 | if strategy._should_log_debug: |
---|
550 | strategy.logger.debug("loading %s" % mapperutil.state_attribute_str(state, self.key)) |
---|
551 | |
---|
552 | session = sessionlib._state_session(state) |
---|
553 | if session is None: |
---|
554 | raise sa_exc.UnboundExecutionError( |
---|
555 | "Parent instance %s is not bound to a Session; " |
---|
556 | "lazy load operation of attribute '%s' cannot proceed" % |
---|
557 | (mapperutil.state_str(state), self.key) |
---|
558 | ) |
---|
559 | |
---|
560 | q = session.query(prop.mapper)._adapt_all_clauses() |
---|
561 | |
---|
562 | if state.load_path: |
---|
563 | q = q._with_current_path(state.load_path + (self.key,)) |
---|
564 | |
---|
565 | # if we have a simple primary key load, use mapper.get() |
---|
566 | # to possibly save a DB round trip |
---|
567 | if strategy.use_get: |
---|
568 | ident = [] |
---|
569 | allnulls = True |
---|
570 | for primary_key in prop.mapper.primary_key: |
---|
571 | val = instance_mapper._get_committed_state_attr_by_column(state, strategy._equated_columns[primary_key]) |
---|
572 | allnulls = allnulls and val is None |
---|
573 | ident.append(val) |
---|
574 | if allnulls: |
---|
575 | return None |
---|
576 | if state.load_options: |
---|
577 | q = q._conditional_options(*state.load_options) |
---|
578 | return q.get(ident) |
---|
579 | |
---|
580 | if prop.order_by: |
---|
581 | q = q.order_by(*util.to_list(prop.order_by)) |
---|
582 | |
---|
583 | if state.load_options: |
---|
584 | q = q._conditional_options(*state.load_options) |
---|
585 | q = q.filter(strategy.lazy_clause(state)) |
---|
586 | |
---|
587 | result = q.all() |
---|
588 | if strategy.uselist: |
---|
589 | return result |
---|
590 | else: |
---|
591 | if result: |
---|
592 | return result[0] |
---|
593 | else: |
---|
594 | return None |
---|
595 | |
---|
596 | class EagerLoader(AbstractRelationLoader): |
---|
597 | """Strategize a relation() that loads within the process of the parent object being selected.""" |
---|
598 | |
---|
599 | def init(self): |
---|
600 | super(EagerLoader, self).init() |
---|
601 | self.join_depth = self.parent_property.join_depth |
---|
602 | |
---|
603 | def init_class_attribute(self, mapper): |
---|
604 | self.parent_property._get_strategy(LazyLoader).init_class_attribute(mapper) |
---|
605 | |
---|
606 | def setup_query(self, context, entity, path, adapter, column_collection=None, parentmapper=None, **kwargs): |
---|
607 | """Add a left outer join to the statement thats being constructed.""" |
---|
608 | |
---|
609 | if not context.enable_eagerloads: |
---|
610 | return |
---|
611 | |
---|
612 | path = path + (self.key,) |
---|
613 | |
---|
614 | # check for user-defined eager alias |
---|
615 | if ("user_defined_eager_row_processor", path) in context.attributes: |
---|
616 | clauses = context.attributes[("user_defined_eager_row_processor", path)] |
---|
617 | |
---|
618 | adapter = entity._get_entity_clauses(context.query, context) |
---|
619 | if adapter and clauses: |
---|
620 | context.attributes[("user_defined_eager_row_processor", path)] = clauses = clauses.wrap(adapter) |
---|
621 | elif adapter: |
---|
622 | context.attributes[("user_defined_eager_row_processor", path)] = clauses = adapter |
---|
623 | |
---|
624 | add_to_collection = context.primary_columns |
---|
625 | |
---|
626 | else: |
---|
627 | clauses = self._create_eager_join(context, entity, path, adapter, parentmapper) |
---|
628 | if not clauses: |
---|
629 | return |
---|
630 | |
---|
631 | context.attributes[("eager_row_processor", path)] = clauses |
---|
632 | |
---|
633 | add_to_collection = context.secondary_columns |
---|
634 | |
---|
635 | for value in self.mapper._iterate_polymorphic_properties(): |
---|
636 | value.setup(context, entity, path + (self.mapper.base_mapper,), clauses, parentmapper=self.mapper, column_collection=add_to_collection) |
---|
637 | |
---|
638 | def _create_eager_join(self, context, entity, path, adapter, parentmapper): |
---|
639 | # check for join_depth or basic recursion, |
---|
640 | # if the current path was not explicitly stated as |
---|
641 | # a desired "loaderstrategy" (i.e. via query.options()) |
---|
642 | if ("loaderstrategy", path) not in context.attributes: |
---|
643 | if self.join_depth: |
---|
644 | if len(path) / 2 > self.join_depth: |
---|
645 | return |
---|
646 | else: |
---|
647 | if self.mapper.base_mapper in path: |
---|
648 | return |
---|
649 | |
---|
650 | if parentmapper is None: |
---|
651 | localparent = entity.mapper |
---|
652 | else: |
---|
653 | localparent = parentmapper |
---|
654 | |
---|
655 | # whether or not the Query will wrap the selectable in a subquery, |
---|
656 | # and then attach eager load joins to that (i.e., in the case of LIMIT/OFFSET etc.) |
---|
657 | should_nest_selectable = context.query._should_nest_selectable |
---|
658 | |
---|
659 | if entity in context.eager_joins: |
---|
660 | entity_key, default_towrap = entity, entity.selectable |
---|
661 | |
---|
662 | elif should_nest_selectable or not context.from_clause: |
---|
663 | # if no from_clause, or a subquery is going to be generated, |
---|
664 | # store eager joins per _MappedEntity; Query._compile_context will |
---|
665 | # add them as separate selectables to the select(), or splice them together |
---|
666 | # after the subquery is generated |
---|
667 | entity_key, default_towrap = entity, entity.selectable |
---|
668 | else: |
---|
669 | index, clause = sql_util.find_join_source(context.from_clause, entity.selectable) |
---|
670 | if clause: |
---|
671 | # join to an existing FROM clause on the query. |
---|
672 | # key it to its list index in the eager_joins dict. |
---|
673 | # Query._compile_context will adapt as needed and append to the |
---|
674 | # FROM clause of the select(). |
---|
675 | entity_key, default_towrap = index, clause |
---|
676 | else: |
---|
677 | # if no from_clause to join to, |
---|
678 | # store eager joins per _MappedEntity |
---|
679 | entity_key, default_towrap = entity, entity.selectable |
---|
680 | |
---|
681 | towrap = context.eager_joins.setdefault(entity_key, default_towrap) |
---|
682 | |
---|
683 | # create AliasedClauses object to build up the eager query. |
---|
684 | clauses = mapperutil.ORMAdapter(mapperutil.AliasedClass(self.mapper), |
---|
685 | equivalents=self.mapper._equivalent_columns, adapt_required=True) |
---|
686 | |
---|
687 | join_to_left = False |
---|
688 | if adapter: |
---|
689 | if getattr(adapter, 'aliased_class', None): |
---|
690 | onclause = getattr(adapter.aliased_class, self.key, self.parent_property) |
---|
691 | else: |
---|
692 | onclause = getattr(mapperutil.AliasedClass(self.parent, adapter.selectable), self.key, self.parent_property) |
---|
693 | |
---|
694 | if onclause is self.parent_property: |
---|
695 | # TODO: this is a temporary hack to account for polymorphic eager loads where |
---|
696 | # the eagerload is referencing via of_type(). |
---|
697 | join_to_left = True |
---|
698 | else: |
---|
699 | onclause = self.parent_property |
---|
700 | |
---|
701 | context.eager_joins[entity_key] = eagerjoin = mapperutil.outerjoin(towrap, clauses.aliased_class, onclause, join_to_left=join_to_left) |
---|
702 | |
---|
703 | # send a hint to the Query as to where it may "splice" this join |
---|
704 | eagerjoin.stop_on = entity.selectable |
---|
705 | |
---|
706 | if not self.parent_property.secondary and context.query._should_nest_selectable and not parentmapper: |
---|
707 | # for parentclause that is the non-eager end of the join, |
---|
708 | # ensure all the parent cols in the primaryjoin are actually in the |
---|
709 | # columns clause (i.e. are not deferred), so that aliasing applied by the Query propagates |
---|
710 | # those columns outward. This has the effect of "undefering" those columns. |
---|
711 | for col in sql_util.find_columns(self.parent_property.primaryjoin): |
---|
712 | if localparent.mapped_table.c.contains_column(col): |
---|
713 | if adapter: |
---|
714 | col = adapter.columns[col] |
---|
715 | context.primary_columns.append(col) |
---|
716 | |
---|
717 | if self.parent_property.order_by: |
---|
718 | context.eager_order_by += eagerjoin._target_adapter.copy_and_process(util.to_list(self.parent_property.order_by)) |
---|
719 | |
---|
720 | return clauses |
---|
721 | |
---|
722 | def _create_eager_adapter(self, context, row, adapter, path): |
---|
723 | if ("user_defined_eager_row_processor", path) in context.attributes: |
---|
724 | decorator = context.attributes[("user_defined_eager_row_processor", path)] |
---|
725 | # user defined eagerloads are part of the "primary" portion of the load. |
---|
726 | # the adapters applied to the Query should be honored. |
---|
727 | if context.adapter and decorator: |
---|
728 | decorator = decorator.wrap(context.adapter) |
---|
729 | elif context.adapter: |
---|
730 | decorator = context.adapter |
---|
731 | elif ("eager_row_processor", path) in context.attributes: |
---|
732 | decorator = context.attributes[("eager_row_processor", path)] |
---|
733 | else: |
---|
734 | if self._should_log_debug: |
---|
735 | self.logger.debug("Could not locate aliased clauses for key: " + str(path)) |
---|
736 | return False |
---|
737 | |
---|
738 | try: |
---|
739 | identity_key = self.mapper.identity_key_from_row(row, decorator) |
---|
740 | return decorator |
---|
741 | except KeyError, k: |
---|
742 | # no identity key - dont return a row processor, will cause a degrade to lazy |
---|
743 | if self._should_log_debug: |
---|
744 | self.logger.debug("could not locate identity key from row; missing column '%s'" % k) |
---|
745 | return False |
---|
746 | |
---|
747 | def create_row_processor(self, context, path, mapper, row, adapter): |
---|
748 | path = path + (self.key,) |
---|
749 | |
---|
750 | eager_adapter = self._create_eager_adapter(context, row, adapter, path) |
---|
751 | |
---|
752 | if eager_adapter is not False: |
---|
753 | key = self.key |
---|
754 | _instance = self.mapper._instance_processor(context, path + (self.mapper.base_mapper,), eager_adapter) |
---|
755 | |
---|
756 | if not self.uselist: |
---|
757 | def execute(state, dict_, row, isnew, **flags): |
---|
758 | if isnew: |
---|
759 | # set a scalar object instance directly on the |
---|
760 | # parent object, bypassing InstrumentedAttribute |
---|
761 | # event handlers. |
---|
762 | dict_[key] = _instance(row, None) |
---|
763 | else: |
---|
764 | # call _instance on the row, even though the object has been created, |
---|
765 | # so that we further descend into properties |
---|
766 | _instance(row, None) |
---|
767 | else: |
---|
768 | def execute(state, dict_, row, isnew, **flags): |
---|
769 | if isnew or (state, key) not in context.attributes: |
---|
770 | # appender_key can be absent from context.attributes with isnew=False |
---|
771 | # when self-referential eager loading is used; the same instance may be present |
---|
772 | # in two distinct sets of result columns |
---|
773 | |
---|
774 | collection = attributes.init_state_collection(state, dict_, key) |
---|
775 | appender = util.UniqueAppender(collection, 'append_without_event') |
---|
776 | |
---|
777 | context.attributes[(state, key)] = appender |
---|
778 | |
---|
779 | result_list = context.attributes[(state, key)] |
---|
780 | |
---|
781 | _instance(row, result_list) |
---|
782 | |
---|
783 | if self._should_log_debug: |
---|
784 | execute = self.debug_callable(execute, self.logger, |
---|
785 | "%s returning eager instance loader" % self, |
---|
786 | lambda state, dict_, row, isnew, **flags: "%s eagerload %s" % \ |
---|
787 | (self, |
---|
788 | self.uselist and "scalar attribute" |
---|
789 | or "collection") |
---|
790 | ) |
---|
791 | |
---|
792 | return (execute, execute) |
---|
793 | else: |
---|
794 | if self._should_log_debug: |
---|
795 | self.logger.debug("%s degrading to lazy loader" % self) |
---|
796 | return self.parent_property._get_strategy(LazyLoader).create_row_processor(context, path, mapper, row, adapter) |
---|
797 | |
---|
798 | log.class_logger(EagerLoader) |
---|
799 | |
---|
800 | class EagerLazyOption(StrategizedOption): |
---|
801 | |
---|
802 | def __init__(self, key, lazy=True, chained=False, mapper=None, propagate_to_loaders=True): |
---|
803 | super(EagerLazyOption, self).__init__(key, mapper) |
---|
804 | self.lazy = lazy |
---|
805 | self.chained = chained |
---|
806 | self.propagate_to_loaders = propagate_to_loaders |
---|
807 | |
---|
808 | def is_chained(self): |
---|
809 | return not self.lazy and self.chained |
---|
810 | |
---|
811 | def get_strategy_class(self): |
---|
812 | if self.lazy: |
---|
813 | return LazyLoader |
---|
814 | elif self.lazy is False: |
---|
815 | return EagerLoader |
---|
816 | elif self.lazy is None: |
---|
817 | return NoLoader |
---|
818 | |
---|
819 | class LoadEagerFromAliasOption(PropertyOption): |
---|
820 | |
---|
821 | def __init__(self, key, alias=None): |
---|
822 | super(LoadEagerFromAliasOption, self).__init__(key) |
---|
823 | if alias: |
---|
824 | if not isinstance(alias, basestring): |
---|
825 | m, alias, is_aliased_class = mapperutil._entity_info(alias) |
---|
826 | self.alias = alias |
---|
827 | |
---|
828 | def process_query_property(self, query, paths, mappers): |
---|
829 | if self.alias: |
---|
830 | if isinstance(self.alias, basestring): |
---|
831 | mapper = mappers[-1] |
---|
832 | (root_mapper, propname) = paths[-1][-2:] |
---|
833 | prop = mapper.get_property(propname, resolve_synonyms=True) |
---|
834 | self.alias = prop.target.alias(self.alias) |
---|
835 | query._attributes[("user_defined_eager_row_processor", paths[-1])] = sql_util.ColumnAdapter(self.alias) |
---|
836 | else: |
---|
837 | (root_mapper, propname) = paths[-1][-2:] |
---|
838 | mapper = mappers[-1] |
---|
839 | prop = mapper.get_property(propname, resolve_synonyms=True) |
---|
840 | adapter = query._polymorphic_adapters.get(prop.mapper, None) |
---|
841 | query._attributes[("user_defined_eager_row_processor", paths[-1])] = adapter |
---|
842 | |
---|
843 | class _SingleParentValidator(interfaces.AttributeExtension): |
---|
844 | def __init__(self, prop): |
---|
845 | self.prop = prop |
---|
846 | |
---|
847 | def _do_check(self, state, value, oldvalue, initiator): |
---|
848 | if value is not None: |
---|
849 | hasparent = initiator.hasparent(attributes.instance_state(value)) |
---|
850 | if hasparent and oldvalue is not value: |
---|
851 | raise sa_exc.InvalidRequestError("Instance %s is already associated with an instance " |
---|
852 | "of %s via its %s attribute, and is only allowed a single parent." % |
---|
853 | (mapperutil.instance_str(value), state.class_, self.prop) |
---|
854 | ) |
---|
855 | return value |
---|
856 | |
---|
857 | def append(self, state, value, initiator): |
---|
858 | return self._do_check(state, value, None, initiator) |
---|
859 | |
---|
860 | def set(self, state, value, oldvalue, initiator): |
---|
861 | return self._do_check(state, value, oldvalue, initiator) |
---|
862 | |
---|
863 | |
---|