1 | # properties.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 | """MapperProperty implementations. |
---|
8 | |
---|
9 | This is a private module which defines the behavior of invidual ORM-mapped |
---|
10 | attributes. |
---|
11 | |
---|
12 | """ |
---|
13 | |
---|
14 | from sqlalchemy import sql, util, log |
---|
15 | import sqlalchemy.exceptions as sa_exc |
---|
16 | from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, join_condition |
---|
17 | from sqlalchemy.sql import operators, expression |
---|
18 | from sqlalchemy.orm import ( |
---|
19 | attributes, dependency, mapper, object_mapper, strategies, |
---|
20 | ) |
---|
21 | from sqlalchemy.orm.util import CascadeOptions, _class_to_mapper, _orm_annotate, _orm_deannotate |
---|
22 | from sqlalchemy.orm.interfaces import ( |
---|
23 | MANYTOMANY, MANYTOONE, MapperProperty, ONETOMANY, PropComparator, |
---|
24 | StrategizedProperty, |
---|
25 | ) |
---|
26 | |
---|
27 | __all__ = ('ColumnProperty', 'CompositeProperty', 'SynonymProperty', |
---|
28 | 'ComparableProperty', 'RelationProperty', 'BackRef') |
---|
29 | |
---|
30 | |
---|
31 | class ColumnProperty(StrategizedProperty): |
---|
32 | """Describes an object attribute that corresponds to a table column.""" |
---|
33 | |
---|
34 | def __init__(self, *columns, **kwargs): |
---|
35 | """Construct a ColumnProperty. |
---|
36 | |
---|
37 | :param \*columns: The list of `columns` describes a single |
---|
38 | object property. If there are multiple tables joined |
---|
39 | together for the mapper, this list represents the equivalent |
---|
40 | column as it appears across each table. |
---|
41 | |
---|
42 | :param group: |
---|
43 | |
---|
44 | :param deferred: |
---|
45 | |
---|
46 | :param comparator_factory: |
---|
47 | |
---|
48 | :param descriptor: |
---|
49 | |
---|
50 | :param extension: |
---|
51 | |
---|
52 | """ |
---|
53 | self.columns = [expression._labeled(c) for c in columns] |
---|
54 | self.group = kwargs.pop('group', None) |
---|
55 | self.deferred = kwargs.pop('deferred', False) |
---|
56 | self.no_instrument = kwargs.pop('_no_instrument', False) |
---|
57 | self.comparator_factory = kwargs.pop('comparator_factory', self.__class__.Comparator) |
---|
58 | self.descriptor = kwargs.pop('descriptor', None) |
---|
59 | self.extension = kwargs.pop('extension', None) |
---|
60 | if kwargs: |
---|
61 | raise TypeError( |
---|
62 | "%s received unexpected keyword argument(s): %s" % ( |
---|
63 | self.__class__.__name__, ', '.join(sorted(kwargs.keys())))) |
---|
64 | |
---|
65 | util.set_creation_order(self) |
---|
66 | if self.no_instrument: |
---|
67 | self.strategy_class = strategies.UninstrumentedColumnLoader |
---|
68 | elif self.deferred: |
---|
69 | self.strategy_class = strategies.DeferredColumnLoader |
---|
70 | else: |
---|
71 | self.strategy_class = strategies.ColumnLoader |
---|
72 | |
---|
73 | def instrument_class(self, mapper): |
---|
74 | if self.no_instrument: |
---|
75 | return |
---|
76 | |
---|
77 | attributes.register_descriptor( |
---|
78 | mapper.class_, |
---|
79 | self.key, |
---|
80 | comparator=self.comparator_factory(self, mapper), |
---|
81 | parententity=mapper, |
---|
82 | property_=self |
---|
83 | ) |
---|
84 | |
---|
85 | def do_init(self): |
---|
86 | super(ColumnProperty, self).do_init() |
---|
87 | if len(self.columns) > 1 and self.parent.primary_key.issuperset(self.columns): |
---|
88 | util.warn( |
---|
89 | ("On mapper %s, primary key column '%s' is being combined " |
---|
90 | "with distinct primary key column '%s' in attribute '%s'. " |
---|
91 | "Use explicit properties to give each column its own mapped " |
---|
92 | "attribute name.") % (self.parent, self.columns[1], |
---|
93 | self.columns[0], self.key)) |
---|
94 | |
---|
95 | def copy(self): |
---|
96 | return ColumnProperty(deferred=self.deferred, group=self.group, *self.columns) |
---|
97 | |
---|
98 | def getattr(self, state, column): |
---|
99 | return state.get_impl(self.key).get(state, state.dict) |
---|
100 | |
---|
101 | def getcommitted(self, state, column, passive=False): |
---|
102 | return state.get_impl(self.key).get_committed_value(state, state.dict, passive=passive) |
---|
103 | |
---|
104 | def setattr(self, state, value, column): |
---|
105 | state.get_impl(self.key).set(state, state.dict, value, None) |
---|
106 | |
---|
107 | def merge(self, session, source, dest, dont_load, _recursive): |
---|
108 | value = attributes.instance_state(source).value_as_iterable( |
---|
109 | self.key, passive=True) |
---|
110 | if value: |
---|
111 | setattr(dest, self.key, value[0]) |
---|
112 | else: |
---|
113 | attributes.instance_state(dest).expire_attributes([self.key]) |
---|
114 | |
---|
115 | def get_col_value(self, column, value): |
---|
116 | return value |
---|
117 | |
---|
118 | class Comparator(PropComparator): |
---|
119 | @util.memoized_instancemethod |
---|
120 | def __clause_element__(self): |
---|
121 | if self.adapter: |
---|
122 | return self.adapter(self.prop.columns[0]) |
---|
123 | else: |
---|
124 | return self.prop.columns[0]._annotate({"parententity": self.mapper, "parentmapper":self.mapper}) |
---|
125 | |
---|
126 | def operate(self, op, *other, **kwargs): |
---|
127 | return op(self.__clause_element__(), *other, **kwargs) |
---|
128 | |
---|
129 | def reverse_operate(self, op, other, **kwargs): |
---|
130 | col = self.__clause_element__() |
---|
131 | return op(col._bind_param(other), col, **kwargs) |
---|
132 | |
---|
133 | # TODO: legacy..do we need this ? (0.5) |
---|
134 | ColumnComparator = Comparator |
---|
135 | |
---|
136 | def __str__(self): |
---|
137 | return str(self.parent.class_.__name__) + "." + self.key |
---|
138 | |
---|
139 | log.class_logger(ColumnProperty) |
---|
140 | |
---|
141 | class CompositeProperty(ColumnProperty): |
---|
142 | """subclasses ColumnProperty to provide composite type support.""" |
---|
143 | |
---|
144 | def __init__(self, class_, *columns, **kwargs): |
---|
145 | if 'comparator' in kwargs: |
---|
146 | util.warn_deprecated("The 'comparator' argument to CompositeProperty is deprecated. Use comparator_factory.") |
---|
147 | kwargs['comparator_factory'] = kwargs['comparator'] |
---|
148 | super(CompositeProperty, self).__init__(*columns, **kwargs) |
---|
149 | self._col_position_map = util.column_dict((c, i) for i, c in enumerate(columns)) |
---|
150 | self.composite_class = class_ |
---|
151 | self.strategy_class = strategies.CompositeColumnLoader |
---|
152 | |
---|
153 | def copy(self): |
---|
154 | return CompositeProperty(deferred=self.deferred, group=self.group, composite_class=self.composite_class, *self.columns) |
---|
155 | |
---|
156 | def do_init(self): |
---|
157 | # skip over ColumnProperty's do_init(), |
---|
158 | # which issues assertions that do not apply to CompositeColumnProperty |
---|
159 | super(ColumnProperty, self).do_init() |
---|
160 | |
---|
161 | def getattr(self, state, column): |
---|
162 | obj = state.get_impl(self.key).get(state, state.dict) |
---|
163 | return self.get_col_value(column, obj) |
---|
164 | |
---|
165 | def getcommitted(self, state, column, passive=False): |
---|
166 | # TODO: no coverage here |
---|
167 | obj = state.get_impl(self.key).get_committed_value(state, state.dict, passive=passive) |
---|
168 | return self.get_col_value(column, obj) |
---|
169 | |
---|
170 | def setattr(self, state, value, column): |
---|
171 | |
---|
172 | obj = state.get_impl(self.key).get(state, state.dict) |
---|
173 | if obj is None: |
---|
174 | obj = self.composite_class(*[None for c in self.columns]) |
---|
175 | state.get_impl(self.key).set(state, state.dict, obj, None) |
---|
176 | |
---|
177 | if hasattr(obj, '__set_composite_values__'): |
---|
178 | values = list(obj.__composite_values__()) |
---|
179 | values[self._col_position_map[column]] = value |
---|
180 | obj.__set_composite_values__(*values) |
---|
181 | else: |
---|
182 | setattr(obj, column.key, value) |
---|
183 | |
---|
184 | def get_col_value(self, column, value): |
---|
185 | if value is None: |
---|
186 | return None |
---|
187 | for a, b in zip(self.columns, value.__composite_values__()): |
---|
188 | if a is column: |
---|
189 | return b |
---|
190 | |
---|
191 | class Comparator(PropComparator): |
---|
192 | def __clause_element__(self): |
---|
193 | if self.adapter: |
---|
194 | # TODO: test coverage for adapted composite comparison |
---|
195 | return expression.ClauseList(*[self.adapter(x) for x in self.prop.columns]) |
---|
196 | else: |
---|
197 | return expression.ClauseList(*self.prop.columns) |
---|
198 | |
---|
199 | __hash__ = None |
---|
200 | |
---|
201 | def __eq__(self, other): |
---|
202 | if other is None: |
---|
203 | values = [None] * len(self.prop.columns) |
---|
204 | else: |
---|
205 | values = other.__composite_values__() |
---|
206 | return sql.and_(*[a==b for a, b in zip(self.prop.columns, values)]) |
---|
207 | |
---|
208 | def __ne__(self, other): |
---|
209 | return sql.not_(self.__eq__(other)) |
---|
210 | |
---|
211 | def __str__(self): |
---|
212 | return str(self.parent.class_.__name__) + "." + self.key |
---|
213 | |
---|
214 | class ConcreteInheritedProperty(MapperProperty): |
---|
215 | extension = None |
---|
216 | |
---|
217 | def setup(self, context, entity, path, adapter, **kwargs): |
---|
218 | pass |
---|
219 | |
---|
220 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
221 | return (None, None) |
---|
222 | |
---|
223 | def instrument_class(self, mapper): |
---|
224 | def warn(): |
---|
225 | raise AttributeError("Concrete %s does not implement attribute %r at " |
---|
226 | "the instance level. Add this property explicitly to %s." % |
---|
227 | (self.parent, self.key, self.parent)) |
---|
228 | |
---|
229 | class NoninheritedConcreteProp(object): |
---|
230 | def __set__(s, obj, value): |
---|
231 | warn() |
---|
232 | def __delete__(s, obj): |
---|
233 | warn() |
---|
234 | def __get__(s, obj, owner): |
---|
235 | warn() |
---|
236 | |
---|
237 | comparator_callable = None |
---|
238 | # TODO: put this process into a deferred callable? |
---|
239 | for m in self.parent.iterate_to_root(): |
---|
240 | p = m._get_property(self.key) |
---|
241 | if not isinstance(p, ConcreteInheritedProperty): |
---|
242 | comparator_callable = p.comparator_factory |
---|
243 | break |
---|
244 | |
---|
245 | attributes.register_descriptor( |
---|
246 | mapper.class_, |
---|
247 | self.key, |
---|
248 | comparator=comparator_callable(self, mapper), |
---|
249 | parententity=mapper, |
---|
250 | property_=self, |
---|
251 | proxy_property=NoninheritedConcreteProp() |
---|
252 | ) |
---|
253 | |
---|
254 | |
---|
255 | class SynonymProperty(MapperProperty): |
---|
256 | |
---|
257 | extension = None |
---|
258 | |
---|
259 | def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None): |
---|
260 | self.name = name |
---|
261 | self.map_column = map_column |
---|
262 | self.descriptor = descriptor |
---|
263 | self.comparator_factory = comparator_factory |
---|
264 | util.set_creation_order(self) |
---|
265 | |
---|
266 | def setup(self, context, entity, path, adapter, **kwargs): |
---|
267 | pass |
---|
268 | |
---|
269 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
270 | return (None, None) |
---|
271 | |
---|
272 | def instrument_class(self, mapper): |
---|
273 | class_ = self.parent.class_ |
---|
274 | |
---|
275 | if self.descriptor is None: |
---|
276 | class SynonymProp(object): |
---|
277 | def __set__(s, obj, value): |
---|
278 | setattr(obj, self.name, value) |
---|
279 | def __delete__(s, obj): |
---|
280 | delattr(obj, self.name) |
---|
281 | def __get__(s, obj, owner): |
---|
282 | if obj is None: |
---|
283 | return s |
---|
284 | return getattr(obj, self.name) |
---|
285 | |
---|
286 | self.descriptor = SynonymProp() |
---|
287 | |
---|
288 | def comparator_callable(prop, mapper): |
---|
289 | def comparator(): |
---|
290 | prop = self.parent._get_property(self.key, resolve_synonyms=True) |
---|
291 | if self.comparator_factory: |
---|
292 | return self.comparator_factory(prop, mapper) |
---|
293 | else: |
---|
294 | return prop.comparator_factory(prop, mapper) |
---|
295 | return comparator |
---|
296 | |
---|
297 | attributes.register_descriptor( |
---|
298 | mapper.class_, |
---|
299 | self.key, |
---|
300 | comparator=comparator_callable(self, mapper), |
---|
301 | parententity=mapper, |
---|
302 | property_=self, |
---|
303 | proxy_property=self.descriptor |
---|
304 | ) |
---|
305 | |
---|
306 | def merge(self, session, source, dest, dont_load, _recursive): |
---|
307 | pass |
---|
308 | |
---|
309 | log.class_logger(SynonymProperty) |
---|
310 | |
---|
311 | class ComparableProperty(MapperProperty): |
---|
312 | """Instruments a Python property for use in query expressions.""" |
---|
313 | |
---|
314 | extension = None |
---|
315 | |
---|
316 | def __init__(self, comparator_factory, descriptor=None): |
---|
317 | self.descriptor = descriptor |
---|
318 | self.comparator_factory = comparator_factory |
---|
319 | util.set_creation_order(self) |
---|
320 | |
---|
321 | def instrument_class(self, mapper): |
---|
322 | """Set up a proxy to the unmanaged descriptor.""" |
---|
323 | |
---|
324 | attributes.register_descriptor( |
---|
325 | mapper.class_, |
---|
326 | self.key, |
---|
327 | comparator=self.comparator_factory(self, mapper), |
---|
328 | parententity=mapper, |
---|
329 | property_=self, |
---|
330 | proxy_property=self.descriptor |
---|
331 | ) |
---|
332 | |
---|
333 | def setup(self, context, entity, path, adapter, **kwargs): |
---|
334 | pass |
---|
335 | |
---|
336 | def create_row_processor(self, selectcontext, path, mapper, row, adapter): |
---|
337 | return (None, None) |
---|
338 | |
---|
339 | def merge(self, session, source, dest, dont_load, _recursive): |
---|
340 | pass |
---|
341 | |
---|
342 | |
---|
343 | class RelationProperty(StrategizedProperty): |
---|
344 | """Describes an object property that holds a single item or list |
---|
345 | of items that correspond to a related database table. |
---|
346 | """ |
---|
347 | |
---|
348 | def __init__(self, argument, |
---|
349 | secondary=None, primaryjoin=None, |
---|
350 | secondaryjoin=None, |
---|
351 | foreign_keys=None, |
---|
352 | uselist=None, |
---|
353 | order_by=False, |
---|
354 | backref=None, |
---|
355 | back_populates=None, |
---|
356 | post_update=False, |
---|
357 | cascade=False, extension=None, |
---|
358 | viewonly=False, lazy=True, |
---|
359 | collection_class=None, passive_deletes=False, |
---|
360 | passive_updates=True, remote_side=None, |
---|
361 | enable_typechecks=True, join_depth=None, |
---|
362 | comparator_factory=None, |
---|
363 | single_parent=False, |
---|
364 | strategy_class=None, _local_remote_pairs=None, query_class=None): |
---|
365 | |
---|
366 | self.uselist = uselist |
---|
367 | self.argument = argument |
---|
368 | self.secondary = secondary |
---|
369 | self.primaryjoin = primaryjoin |
---|
370 | self.secondaryjoin = secondaryjoin |
---|
371 | self.post_update = post_update |
---|
372 | self.direction = None |
---|
373 | self.viewonly = viewonly |
---|
374 | self.lazy = lazy |
---|
375 | self.single_parent = single_parent |
---|
376 | self._foreign_keys = foreign_keys |
---|
377 | self.collection_class = collection_class |
---|
378 | self.passive_deletes = passive_deletes |
---|
379 | self.passive_updates = passive_updates |
---|
380 | self.remote_side = remote_side |
---|
381 | self.enable_typechecks = enable_typechecks |
---|
382 | self.query_class = query_class |
---|
383 | |
---|
384 | self.join_depth = join_depth |
---|
385 | self.local_remote_pairs = _local_remote_pairs |
---|
386 | self.extension = extension |
---|
387 | self.comparator_factory = comparator_factory or RelationProperty.Comparator |
---|
388 | self.comparator = self.comparator_factory(self, None) |
---|
389 | util.set_creation_order(self) |
---|
390 | |
---|
391 | if strategy_class: |
---|
392 | self.strategy_class = strategy_class |
---|
393 | elif self.lazy == 'dynamic': |
---|
394 | from sqlalchemy.orm import dynamic |
---|
395 | self.strategy_class = dynamic.DynaLoader |
---|
396 | elif self.lazy is False: |
---|
397 | self.strategy_class = strategies.EagerLoader |
---|
398 | elif self.lazy is None: |
---|
399 | self.strategy_class = strategies.NoLoader |
---|
400 | else: |
---|
401 | self.strategy_class = strategies.LazyLoader |
---|
402 | |
---|
403 | self._reverse_property = set() |
---|
404 | |
---|
405 | if cascade is not False: |
---|
406 | self.cascade = CascadeOptions(cascade) |
---|
407 | else: |
---|
408 | self.cascade = CascadeOptions("save-update, merge") |
---|
409 | |
---|
410 | if self.passive_deletes == 'all' and ("delete" in self.cascade or "delete-orphan" in self.cascade): |
---|
411 | raise sa_exc.ArgumentError("Can't set passive_deletes='all' in conjunction with 'delete' or 'delete-orphan' cascade") |
---|
412 | |
---|
413 | self.order_by = order_by |
---|
414 | |
---|
415 | self.back_populates = back_populates |
---|
416 | |
---|
417 | if self.back_populates: |
---|
418 | if backref: |
---|
419 | raise sa_exc.ArgumentError("backref and back_populates keyword arguments are mutually exclusive") |
---|
420 | self.backref = None |
---|
421 | elif isinstance(backref, basestring): |
---|
422 | # propagate explicitly sent primary/secondary join conditions to the BackRef object if |
---|
423 | # just a string was sent |
---|
424 | if secondary is not None: |
---|
425 | # reverse primary/secondary in case of a many-to-many |
---|
426 | self.backref = BackRef(backref, primaryjoin=secondaryjoin, |
---|
427 | secondaryjoin=primaryjoin, passive_updates=self.passive_updates) |
---|
428 | else: |
---|
429 | self.backref = BackRef(backref, primaryjoin=primaryjoin, |
---|
430 | secondaryjoin=secondaryjoin, passive_updates=self.passive_updates) |
---|
431 | else: |
---|
432 | self.backref = backref |
---|
433 | |
---|
434 | def instrument_class(self, mapper): |
---|
435 | attributes.register_descriptor( |
---|
436 | mapper.class_, |
---|
437 | self.key, |
---|
438 | comparator=self.comparator_factory(self, mapper), |
---|
439 | parententity=mapper, |
---|
440 | property_=self |
---|
441 | ) |
---|
442 | |
---|
443 | class Comparator(PropComparator): |
---|
444 | def __init__(self, prop, mapper, of_type=None, adapter=None): |
---|
445 | self.prop = prop |
---|
446 | self.mapper = mapper |
---|
447 | self.adapter = adapter |
---|
448 | if of_type: |
---|
449 | self._of_type = _class_to_mapper(of_type) |
---|
450 | |
---|
451 | def adapted(self, adapter): |
---|
452 | """Return a copy of this PropComparator which will use the given adaption function |
---|
453 | on the local side of generated expressions. |
---|
454 | |
---|
455 | """ |
---|
456 | return self.__class__(self.property, self.mapper, getattr(self, '_of_type', None), adapter) |
---|
457 | |
---|
458 | @property |
---|
459 | def parententity(self): |
---|
460 | return self.property.parent |
---|
461 | |
---|
462 | def __clause_element__(self): |
---|
463 | elem = self.property.parent._with_polymorphic_selectable |
---|
464 | if self.adapter: |
---|
465 | return self.adapter(elem) |
---|
466 | else: |
---|
467 | return elem |
---|
468 | |
---|
469 | def operate(self, op, *other, **kwargs): |
---|
470 | return op(self, *other, **kwargs) |
---|
471 | |
---|
472 | def reverse_operate(self, op, other, **kwargs): |
---|
473 | return op(self, *other, **kwargs) |
---|
474 | |
---|
475 | def of_type(self, cls): |
---|
476 | return RelationProperty.Comparator(self.property, self.mapper, cls, adapter=self.adapter) |
---|
477 | |
---|
478 | def in_(self, other): |
---|
479 | raise NotImplementedError("in_() not yet supported for relations. For a " |
---|
480 | "simple many-to-one, use in_() against the set of foreign key values.") |
---|
481 | |
---|
482 | __hash__ = None |
---|
483 | |
---|
484 | def __eq__(self, other): |
---|
485 | if other is None: |
---|
486 | if self.property.direction in [ONETOMANY, MANYTOMANY]: |
---|
487 | return ~self._criterion_exists() |
---|
488 | else: |
---|
489 | return _orm_annotate(self.property._optimized_compare(None, adapt_source=self.adapter)) |
---|
490 | elif self.property.uselist: |
---|
491 | raise sa_exc.InvalidRequestError("Can't compare a collection to an object or collection; use contains() to test for membership.") |
---|
492 | else: |
---|
493 | return _orm_annotate(self.property._optimized_compare(other, adapt_source=self.adapter)) |
---|
494 | |
---|
495 | def _criterion_exists(self, criterion=None, **kwargs): |
---|
496 | if getattr(self, '_of_type', None): |
---|
497 | target_mapper = self._of_type |
---|
498 | to_selectable = target_mapper._with_polymorphic_selectable |
---|
499 | if self.property._is_self_referential(): |
---|
500 | to_selectable = to_selectable.alias() |
---|
501 | |
---|
502 | single_crit = target_mapper._single_table_criterion |
---|
503 | if single_crit: |
---|
504 | if criterion is not None: |
---|
505 | criterion = single_crit & criterion |
---|
506 | else: |
---|
507 | criterion = single_crit |
---|
508 | else: |
---|
509 | to_selectable = None |
---|
510 | |
---|
511 | if self.adapter: |
---|
512 | source_selectable = self.__clause_element__() |
---|
513 | else: |
---|
514 | source_selectable = None |
---|
515 | |
---|
516 | pj, sj, source, dest, secondary, target_adapter = \ |
---|
517 | self.property._create_joins(dest_polymorphic=True, dest_selectable=to_selectable, source_selectable=source_selectable) |
---|
518 | |
---|
519 | for k in kwargs: |
---|
520 | crit = self.property.mapper.class_manager[k] == kwargs[k] |
---|
521 | if criterion is None: |
---|
522 | criterion = crit |
---|
523 | else: |
---|
524 | criterion = criterion & crit |
---|
525 | |
---|
526 | # annotate the *local* side of the join condition, in the case of pj + sj this |
---|
527 | # is the full primaryjoin, in the case of just pj its the local side of |
---|
528 | # the primaryjoin. |
---|
529 | if sj: |
---|
530 | j = _orm_annotate(pj) & sj |
---|
531 | else: |
---|
532 | j = _orm_annotate(pj, exclude=self.property.remote_side) |
---|
533 | |
---|
534 | if criterion and target_adapter: |
---|
535 | # limit this adapter to annotated only? |
---|
536 | criterion = target_adapter.traverse(criterion) |
---|
537 | |
---|
538 | # only have the "joined left side" of what we return be subject to Query adaption. The right |
---|
539 | # side of it is used for an exists() subquery and should not correlate or otherwise reach out |
---|
540 | # to anything in the enclosing query. |
---|
541 | if criterion: |
---|
542 | criterion = criterion._annotate({'_halt_adapt': True}) |
---|
543 | |
---|
544 | crit = j & criterion |
---|
545 | |
---|
546 | return sql.exists([1], crit, from_obj=dest).correlate(source) |
---|
547 | |
---|
548 | def any(self, criterion=None, **kwargs): |
---|
549 | if not self.property.uselist: |
---|
550 | raise sa_exc.InvalidRequestError("'any()' not implemented for scalar attributes. Use has().") |
---|
551 | |
---|
552 | return self._criterion_exists(criterion, **kwargs) |
---|
553 | |
---|
554 | def has(self, criterion=None, **kwargs): |
---|
555 | if self.property.uselist: |
---|
556 | raise sa_exc.InvalidRequestError("'has()' not implemented for collections. Use any().") |
---|
557 | return self._criterion_exists(criterion, **kwargs) |
---|
558 | |
---|
559 | def contains(self, other, **kwargs): |
---|
560 | if not self.property.uselist: |
---|
561 | raise sa_exc.InvalidRequestError("'contains' not implemented for scalar attributes. Use ==") |
---|
562 | clause = self.property._optimized_compare(other, adapt_source=self.adapter) |
---|
563 | |
---|
564 | if self.property.secondaryjoin: |
---|
565 | clause.negation_clause = self.__negated_contains_or_equals(other) |
---|
566 | |
---|
567 | return clause |
---|
568 | |
---|
569 | def __negated_contains_or_equals(self, other): |
---|
570 | if self.property.direction == MANYTOONE: |
---|
571 | state = attributes.instance_state(other) |
---|
572 | strategy = self.property._get_strategy(strategies.LazyLoader) |
---|
573 | |
---|
574 | def state_bindparam(state, col): |
---|
575 | o = state.obj() # strong ref |
---|
576 | return lambda: self.property.mapper._get_committed_attr_by_column(o, col) |
---|
577 | |
---|
578 | def adapt(col): |
---|
579 | if self.adapter: |
---|
580 | return self.adapter(col) |
---|
581 | else: |
---|
582 | return col |
---|
583 | |
---|
584 | if strategy.use_get: |
---|
585 | return sql.and_(*[ |
---|
586 | sql.or_( |
---|
587 | adapt(x) != state_bindparam(state, y), |
---|
588 | adapt(x) == None) |
---|
589 | for (x, y) in self.property.local_remote_pairs]) |
---|
590 | |
---|
591 | criterion = sql.and_(*[x==y for (x, y) in zip(self.property.mapper.primary_key, self.property.mapper.primary_key_from_instance(other))]) |
---|
592 | return ~self._criterion_exists(criterion) |
---|
593 | |
---|
594 | def __ne__(self, other): |
---|
595 | if other is None: |
---|
596 | if self.property.direction == MANYTOONE: |
---|
597 | return sql.or_(*[x!=None for x in self.property._foreign_keys]) |
---|
598 | else: |
---|
599 | return self._criterion_exists() |
---|
600 | elif self.property.uselist: |
---|
601 | raise sa_exc.InvalidRequestError("Can't compare a collection to an object or collection; use contains() to test for membership.") |
---|
602 | else: |
---|
603 | return self.__negated_contains_or_equals(other) |
---|
604 | |
---|
605 | @util.memoized_property |
---|
606 | def property(self): |
---|
607 | self.prop.parent.compile() |
---|
608 | return self.prop |
---|
609 | |
---|
610 | def compare(self, op, value, value_is_parent=False): |
---|
611 | if op == operators.eq: |
---|
612 | if value is None: |
---|
613 | if self.uselist: |
---|
614 | return ~sql.exists([1], self.primaryjoin) |
---|
615 | else: |
---|
616 | return self._optimized_compare(None, value_is_parent=value_is_parent) |
---|
617 | else: |
---|
618 | return self._optimized_compare(value, value_is_parent=value_is_parent) |
---|
619 | else: |
---|
620 | return op(self.comparator, value) |
---|
621 | |
---|
622 | def _optimized_compare(self, value, value_is_parent=False, adapt_source=None): |
---|
623 | if value is not None: |
---|
624 | value = attributes.instance_state(value) |
---|
625 | return self._get_strategy(strategies.LazyLoader).\ |
---|
626 | lazy_clause(value, reverse_direction=not value_is_parent, alias_secondary=True, adapt_source=adapt_source) |
---|
627 | |
---|
628 | def __str__(self): |
---|
629 | return str(self.parent.class_.__name__) + "." + self.key |
---|
630 | |
---|
631 | def merge(self, session, source, dest, dont_load, _recursive): |
---|
632 | if not dont_load: |
---|
633 | # TODO: no test coverage for recursive check |
---|
634 | for r in self._reverse_property: |
---|
635 | if (source, r) in _recursive: |
---|
636 | return |
---|
637 | |
---|
638 | source_state = attributes.instance_state(source) |
---|
639 | dest_state, dest_dict = attributes.instance_state(dest), attributes.instance_dict(dest) |
---|
640 | |
---|
641 | if not "merge" in self.cascade: |
---|
642 | dest_state.expire_attributes([self.key]) |
---|
643 | return |
---|
644 | |
---|
645 | instances = source_state.value_as_iterable(self.key, passive=True) |
---|
646 | |
---|
647 | if not instances: |
---|
648 | return |
---|
649 | |
---|
650 | if self.uselist: |
---|
651 | dest_list = [] |
---|
652 | for current in instances: |
---|
653 | _recursive[(current, self)] = True |
---|
654 | obj = session._merge(current, dont_load=dont_load, _recursive=_recursive) |
---|
655 | if obj is not None: |
---|
656 | dest_list.append(obj) |
---|
657 | if dont_load: |
---|
658 | coll = attributes.init_collection(dest_state, self.key) |
---|
659 | for c in dest_list: |
---|
660 | coll.append_without_event(c) |
---|
661 | else: |
---|
662 | getattr(dest.__class__, self.key).impl._set_iterable(dest_state, dest_dict, dest_list) |
---|
663 | else: |
---|
664 | current = instances[0] |
---|
665 | if current is not None: |
---|
666 | _recursive[(current, self)] = True |
---|
667 | obj = session._merge(current, dont_load=dont_load, _recursive=_recursive) |
---|
668 | if obj is not None: |
---|
669 | if dont_load: |
---|
670 | dest_state.dict[self.key] = obj |
---|
671 | else: |
---|
672 | setattr(dest, self.key, obj) |
---|
673 | |
---|
674 | def cascade_iterator(self, type_, state, visited_instances, halt_on=None): |
---|
675 | if not type_ in self.cascade: |
---|
676 | return |
---|
677 | |
---|
678 | # only actively lazy load on the 'delete' cascade |
---|
679 | if type_ != 'delete' or self.passive_deletes: |
---|
680 | passive = attributes.PASSIVE_NO_INITIALIZE |
---|
681 | else: |
---|
682 | passive = attributes.PASSIVE_OFF |
---|
683 | |
---|
684 | mapper = self.mapper.primary_mapper() |
---|
685 | instances = state.value_as_iterable(self.key, passive=passive) |
---|
686 | if instances: |
---|
687 | for c in instances: |
---|
688 | if c is not None and c not in visited_instances and (halt_on is None or not halt_on(c)): |
---|
689 | if not isinstance(c, self.mapper.class_): |
---|
690 | raise AssertionError("Attribute '%s' on class '%s' doesn't handle objects " |
---|
691 | "of type '%s'" % (self.key, str(self.parent.class_), str(c.__class__))) |
---|
692 | visited_instances.add(c) |
---|
693 | |
---|
694 | # cascade using the mapper local to this object, so that its individual properties are located |
---|
695 | instance_mapper = object_mapper(c) |
---|
696 | yield (c, instance_mapper, attributes.instance_state(c)) |
---|
697 | |
---|
698 | def _add_reverse_property(self, key): |
---|
699 | other = self.mapper._get_property(key) |
---|
700 | self._reverse_property.add(other) |
---|
701 | other._reverse_property.add(self) |
---|
702 | |
---|
703 | if not other._get_target().common_parent(self.parent): |
---|
704 | raise sa_exc.ArgumentError("reverse_property %r on relation %s references " |
---|
705 | "relation %s, which does not reference mapper %s" % (key, self, other, self.parent)) |
---|
706 | |
---|
707 | if self.direction in (ONETOMANY, MANYTOONE) and self.direction == other.direction: |
---|
708 | raise sa_exc.ArgumentError("%s and back-reference %s are both of the same direction %r." |
---|
709 | " Did you mean to set remote_side on the many-to-one side ?" % (self, other, self.direction)) |
---|
710 | |
---|
711 | def do_init(self): |
---|
712 | self._get_target() |
---|
713 | self._process_dependent_arguments() |
---|
714 | self._determine_joins() |
---|
715 | self._determine_synchronize_pairs() |
---|
716 | self._determine_direction() |
---|
717 | self._determine_local_remote_pairs() |
---|
718 | self._post_init() |
---|
719 | super(RelationProperty, self).do_init() |
---|
720 | |
---|
721 | def _get_target(self): |
---|
722 | if not hasattr(self, 'mapper'): |
---|
723 | if isinstance(self.argument, type): |
---|
724 | self.mapper = mapper.class_mapper(self.argument, compile=False) |
---|
725 | elif isinstance(self.argument, mapper.Mapper): |
---|
726 | self.mapper = self.argument |
---|
727 | elif util.callable(self.argument): |
---|
728 | # accept a callable to suit various deferred-configurational schemes |
---|
729 | self.mapper = mapper.class_mapper(self.argument(), compile=False) |
---|
730 | else: |
---|
731 | raise sa_exc.ArgumentError("relation '%s' expects a class or a mapper argument (received: %s)" % (self.key, type(self.argument))) |
---|
732 | assert isinstance(self.mapper, mapper.Mapper), self.mapper |
---|
733 | return self.mapper |
---|
734 | |
---|
735 | def _process_dependent_arguments(self): |
---|
736 | |
---|
737 | # accept callables for other attributes which may require deferred initialization |
---|
738 | for attr in ('order_by', 'primaryjoin', 'secondaryjoin', 'secondary', '_foreign_keys', 'remote_side'): |
---|
739 | if util.callable(getattr(self, attr)): |
---|
740 | setattr(self, attr, getattr(self, attr)()) |
---|
741 | |
---|
742 | # in the case that InstrumentedAttributes were used to construct |
---|
743 | # primaryjoin or secondaryjoin, remove the "_orm_adapt" annotation so these |
---|
744 | # interact with Query in the same way as the original Table-bound Column objects |
---|
745 | for attr in ('primaryjoin', 'secondaryjoin'): |
---|
746 | val = getattr(self, attr) |
---|
747 | if val is not None: |
---|
748 | util.assert_arg_type(val, sql.ClauseElement, attr) |
---|
749 | setattr(self, attr, _orm_deannotate(val)) |
---|
750 | |
---|
751 | if self.order_by: |
---|
752 | self.order_by = [expression._literal_as_column(x) for x in util.to_list(self.order_by)] |
---|
753 | |
---|
754 | self._foreign_keys = util.column_set(expression._literal_as_column(x) for x in util.to_column_set(self._foreign_keys)) |
---|
755 | self.remote_side = util.column_set(expression._literal_as_column(x) for x in util.to_column_set(self.remote_side)) |
---|
756 | |
---|
757 | if not self.parent.concrete: |
---|
758 | for inheriting in self.parent.iterate_to_root(): |
---|
759 | if inheriting is not self.parent and inheriting._get_property(self.key, raiseerr=False): |
---|
760 | util.warn( |
---|
761 | ("Warning: relation '%s' on mapper '%s' supercedes " |
---|
762 | "the same relation on inherited mapper '%s'; this " |
---|
763 | "can cause dependency issues during flush") % |
---|
764 | (self.key, self.parent, inheriting)) |
---|
765 | |
---|
766 | # TODO: remove 'self.table' |
---|
767 | self.target = self.table = self.mapper.mapped_table |
---|
768 | |
---|
769 | if self.cascade.delete_orphan: |
---|
770 | if self.parent.class_ is self.mapper.class_: |
---|
771 | raise sa_exc.ArgumentError("In relationship '%s', can't establish 'delete-orphan' cascade " |
---|
772 | "rule on a self-referential relationship. " |
---|
773 | "You probably want cascade='all', which includes delete cascading but not orphan detection." %(str(self))) |
---|
774 | self.mapper.primary_mapper().delete_orphans.append((self.key, self.parent.class_)) |
---|
775 | |
---|
776 | def _determine_joins(self): |
---|
777 | if self.secondaryjoin is not None and self.secondary is None: |
---|
778 | raise sa_exc.ArgumentError("Property '" + self.key + "' specified with secondary join condition but no secondary argument") |
---|
779 | # if join conditions were not specified, figure them out based on foreign keys |
---|
780 | |
---|
781 | def _search_for_join(mapper, table): |
---|
782 | # find a join between the given mapper's mapped table and the given table. |
---|
783 | # will try the mapper's local table first for more specificity, then if not |
---|
784 | # found will try the more general mapped table, which in the case of inheritance |
---|
785 | # is a join. |
---|
786 | try: |
---|
787 | return join_condition(mapper.local_table, table) |
---|
788 | except sa_exc.ArgumentError, e: |
---|
789 | return join_condition(mapper.mapped_table, table) |
---|
790 | |
---|
791 | try: |
---|
792 | if self.secondary is not None: |
---|
793 | if self.secondaryjoin is None: |
---|
794 | self.secondaryjoin = _search_for_join(self.mapper, self.secondary) |
---|
795 | if self.primaryjoin is None: |
---|
796 | self.primaryjoin = _search_for_join(self.parent, self.secondary) |
---|
797 | else: |
---|
798 | if self.primaryjoin is None: |
---|
799 | self.primaryjoin = _search_for_join(self.parent, self.target) |
---|
800 | except sa_exc.ArgumentError, e: |
---|
801 | raise sa_exc.ArgumentError("Could not determine join condition between " |
---|
802 | "parent/child tables on relation %s. " |
---|
803 | "Specify a 'primaryjoin' expression. If this is a " |
---|
804 | "many-to-many relation, 'secondaryjoin' is needed as well." % (self)) |
---|
805 | |
---|
806 | def _col_is_part_of_mappings(self, column): |
---|
807 | if self.secondary is None: |
---|
808 | return self.parent.mapped_table.c.contains_column(column) or \ |
---|
809 | self.target.c.contains_column(column) |
---|
810 | else: |
---|
811 | return self.parent.mapped_table.c.contains_column(column) or \ |
---|
812 | self.target.c.contains_column(column) or \ |
---|
813 | self.secondary.c.contains_column(column) is not None |
---|
814 | |
---|
815 | def _determine_synchronize_pairs(self): |
---|
816 | |
---|
817 | if self.local_remote_pairs: |
---|
818 | if not self._foreign_keys: |
---|
819 | raise sa_exc.ArgumentError("foreign_keys argument is required with _local_remote_pairs argument") |
---|
820 | |
---|
821 | self.synchronize_pairs = [] |
---|
822 | |
---|
823 | for l, r in self.local_remote_pairs: |
---|
824 | if r in self._foreign_keys: |
---|
825 | self.synchronize_pairs.append((l, r)) |
---|
826 | elif l in self._foreign_keys: |
---|
827 | self.synchronize_pairs.append((r, l)) |
---|
828 | else: |
---|
829 | eq_pairs = criterion_as_pairs( |
---|
830 | self.primaryjoin, |
---|
831 | consider_as_foreign_keys=self._foreign_keys, |
---|
832 | any_operator=self.viewonly |
---|
833 | ) |
---|
834 | eq_pairs = [ |
---|
835 | (l, r) for l, r in eq_pairs if |
---|
836 | (self._col_is_part_of_mappings(l) and |
---|
837 | self._col_is_part_of_mappings(r)) |
---|
838 | or self.viewonly and r in self._foreign_keys |
---|
839 | ] |
---|
840 | |
---|
841 | if not eq_pairs: |
---|
842 | if not self.viewonly and criterion_as_pairs(self.primaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True): |
---|
843 | raise sa_exc.ArgumentError("Could not locate any equated, locally " |
---|
844 | "mapped column pairs for primaryjoin condition '%s' on relation %s. " |
---|
845 | "For more relaxed rules on join conditions, the relation may be " |
---|
846 | "marked as viewonly=True." % (self.primaryjoin, self) |
---|
847 | ) |
---|
848 | else: |
---|
849 | if self._foreign_keys: |
---|
850 | raise sa_exc.ArgumentError("Could not determine relation direction for " |
---|
851 | "primaryjoin condition '%s', on relation %s. " |
---|
852 | "Do the columns in 'foreign_keys' represent only the 'foreign' columns " |
---|
853 | "in this join condition ?" % (self.primaryjoin, self)) |
---|
854 | else: |
---|
855 | raise sa_exc.ArgumentError("Could not determine relation direction for " |
---|
856 | "primaryjoin condition '%s', on relation %s. " |
---|
857 | "Specify the 'foreign_keys' argument to indicate which columns " |
---|
858 | "on the relation are foreign." % (self.primaryjoin, self)) |
---|
859 | |
---|
860 | self.synchronize_pairs = eq_pairs |
---|
861 | |
---|
862 | if self.secondaryjoin: |
---|
863 | sq_pairs = criterion_as_pairs(self.secondaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=self.viewonly) |
---|
864 | sq_pairs = [(l, r) for l, r in sq_pairs if (self._col_is_part_of_mappings(l) and self._col_is_part_of_mappings(r)) or r in self._foreign_keys] |
---|
865 | |
---|
866 | if not sq_pairs: |
---|
867 | if not self.viewonly and criterion_as_pairs(self.secondaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True): |
---|
868 | raise sa_exc.ArgumentError("Could not locate any equated, locally mapped " |
---|
869 | "column pairs for secondaryjoin condition '%s' on relation %s. " |
---|
870 | "For more relaxed rules on join conditions, the " |
---|
871 | "relation may be marked as viewonly=True." % (self.secondaryjoin, self) |
---|
872 | ) |
---|
873 | else: |
---|
874 | raise sa_exc.ArgumentError("Could not determine relation direction " |
---|
875 | "for secondaryjoin condition '%s', on relation %s. " |
---|
876 | "Specify the foreign_keys argument to indicate which " |
---|
877 | "columns on the relation are foreign." % (self.secondaryjoin, self)) |
---|
878 | |
---|
879 | self.secondary_synchronize_pairs = sq_pairs |
---|
880 | else: |
---|
881 | self.secondary_synchronize_pairs = None |
---|
882 | |
---|
883 | self._foreign_keys = util.column_set(r for l, r in self.synchronize_pairs) |
---|
884 | if self.secondary_synchronize_pairs: |
---|
885 | self._foreign_keys.update(r for l, r in self.secondary_synchronize_pairs) |
---|
886 | |
---|
887 | def _determine_direction(self): |
---|
888 | if self.secondaryjoin is not None: |
---|
889 | self.direction = MANYTOMANY |
---|
890 | elif self._refers_to_parent_table(): |
---|
891 | # self referential defaults to ONETOMANY unless the "remote" side is present |
---|
892 | # and does not reference any foreign key columns |
---|
893 | |
---|
894 | if self.local_remote_pairs: |
---|
895 | remote = [r for l, r in self.local_remote_pairs] |
---|
896 | elif self.remote_side: |
---|
897 | remote = self.remote_side |
---|
898 | else: |
---|
899 | remote = None |
---|
900 | |
---|
901 | if not remote or self._foreign_keys.\ |
---|
902 | difference(l for l, r in self.synchronize_pairs).\ |
---|
903 | intersection(remote): |
---|
904 | self.direction = ONETOMANY |
---|
905 | else: |
---|
906 | self.direction = MANYTOONE |
---|
907 | |
---|
908 | else: |
---|
909 | foreign_keys = [f for c, f in self.synchronize_pairs] |
---|
910 | |
---|
911 | parentcols = util.column_set(self.parent.mapped_table.c) |
---|
912 | targetcols = util.column_set(self.mapper.mapped_table.c) |
---|
913 | |
---|
914 | # fk collection which suggests ONETOMANY. |
---|
915 | onetomany_fk = targetcols.intersection(foreign_keys) |
---|
916 | |
---|
917 | # fk collection which suggests MANYTOONE. |
---|
918 | manytoone_fk = parentcols.intersection(foreign_keys) |
---|
919 | |
---|
920 | if not onetomany_fk and not manytoone_fk: |
---|
921 | raise sa_exc.ArgumentError( |
---|
922 | "Can't determine relation direction for relationship '%s' " |
---|
923 | "- foreign key columns are present in neither the " |
---|
924 | "parent nor the child's mapped tables" % self ) |
---|
925 | |
---|
926 | elif onetomany_fk and manytoone_fk: |
---|
927 | # fks on both sides. do the same |
---|
928 | # test only based on the local side. |
---|
929 | referents = [c for c, f in self.synchronize_pairs] |
---|
930 | onetomany_local = parentcols.intersection(referents) |
---|
931 | manytoone_local = targetcols.intersection(referents) |
---|
932 | |
---|
933 | if onetomany_local and not manytoone_local: |
---|
934 | self.direction = ONETOMANY |
---|
935 | elif manytoone_local and not onetomany_local: |
---|
936 | self.direction = MANYTOONE |
---|
937 | elif onetomany_fk: |
---|
938 | self.direction = ONETOMANY |
---|
939 | elif manytoone_fk: |
---|
940 | self.direction = MANYTOONE |
---|
941 | |
---|
942 | if not self.direction: |
---|
943 | raise sa_exc.ArgumentError( |
---|
944 | "Can't determine relation direction for relationship '%s' " |
---|
945 | "- foreign key columns are present in both the parent and " |
---|
946 | "the child's mapped tables. Specify 'foreign_keys' " |
---|
947 | "argument." % self) |
---|
948 | |
---|
949 | if self.cascade.delete_orphan and not self.single_parent and \ |
---|
950 | (self.direction is MANYTOMANY or self.direction is MANYTOONE): |
---|
951 | util.warn("On %s, delete-orphan cascade is not supported on a " |
---|
952 | "many-to-many or many-to-one relationship when single_parent is not set. " |
---|
953 | " Set single_parent=True on the relation()." % self) |
---|
954 | |
---|
955 | def _determine_local_remote_pairs(self): |
---|
956 | if not self.local_remote_pairs: |
---|
957 | if self.remote_side: |
---|
958 | if self.direction is MANYTOONE: |
---|
959 | self.local_remote_pairs = [ |
---|
960 | (r, l) for l, r in |
---|
961 | criterion_as_pairs(self.primaryjoin, consider_as_referenced_keys=self.remote_side, any_operator=True) |
---|
962 | ] |
---|
963 | else: |
---|
964 | self.local_remote_pairs = criterion_as_pairs(self.primaryjoin, consider_as_foreign_keys=self.remote_side, any_operator=True) |
---|
965 | |
---|
966 | if not self.local_remote_pairs: |
---|
967 | raise sa_exc.ArgumentError("Relation %s could not determine any local/remote column pairs from remote side argument %r" % (self, self.remote_side)) |
---|
968 | |
---|
969 | else: |
---|
970 | if self.viewonly: |
---|
971 | eq_pairs = self.synchronize_pairs |
---|
972 | if self.secondaryjoin: |
---|
973 | eq_pairs += self.secondary_synchronize_pairs |
---|
974 | else: |
---|
975 | eq_pairs = criterion_as_pairs(self.primaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True) |
---|
976 | if self.secondaryjoin: |
---|
977 | eq_pairs += criterion_as_pairs(self.secondaryjoin, consider_as_foreign_keys=self._foreign_keys, any_operator=True) |
---|
978 | eq_pairs = [(l, r) for l, r in eq_pairs if self._col_is_part_of_mappings(l) and self._col_is_part_of_mappings(r)] |
---|
979 | |
---|
980 | if self.direction is MANYTOONE: |
---|
981 | self.local_remote_pairs = [(r, l) for l, r in eq_pairs] |
---|
982 | else: |
---|
983 | self.local_remote_pairs = eq_pairs |
---|
984 | elif self.remote_side: |
---|
985 | raise sa_exc.ArgumentError("remote_side argument is redundant against more detailed _local_remote_side argument.") |
---|
986 | |
---|
987 | for l, r in self.local_remote_pairs: |
---|
988 | |
---|
989 | if self.direction is ONETOMANY and not self._col_is_part_of_mappings(l): |
---|
990 | raise sa_exc.ArgumentError("Local column '%s' is not part of mapping %s. " |
---|
991 | "Specify remote_side argument to indicate which column " |
---|
992 | "lazy join condition should compare against." % (l, self.parent)) |
---|
993 | |
---|
994 | elif self.direction is MANYTOONE and not self._col_is_part_of_mappings(r): |
---|
995 | raise sa_exc.ArgumentError("Remote column '%s' is not part of mapping %s. " |
---|
996 | "Specify remote_side argument to indicate which column lazy " |
---|
997 | "join condition should bind." % (r, self.mapper)) |
---|
998 | |
---|
999 | self.local_side, self.remote_side = [util.ordered_column_set(x) for x in zip(*list(self.local_remote_pairs))] |
---|
1000 | |
---|
1001 | |
---|
1002 | def _post_init(self): |
---|
1003 | if self._should_log_info: |
---|
1004 | self.logger.info(str(self) + " setup primary join %s" % self.primaryjoin) |
---|
1005 | self.logger.info(str(self) + " setup secondary join %s" % self.secondaryjoin) |
---|
1006 | self.logger.info(str(self) + " synchronize pairs [%s]" % ",".join("(%s => %s)" % (l, r) for l, r in self.synchronize_pairs)) |
---|
1007 | self.logger.info(str(self) + " secondary synchronize pairs [%s]" % ",".join(("(%s => %s)" % (l, r) for l, r in self.secondary_synchronize_pairs or []))) |
---|
1008 | self.logger.info(str(self) + " local/remote pairs [%s]" % ",".join("(%s / %s)" % (l, r) for l, r in self.local_remote_pairs)) |
---|
1009 | self.logger.info(str(self) + " relation direction %s" % self.direction) |
---|
1010 | |
---|
1011 | if self.uselist is None and self.direction is MANYTOONE: |
---|
1012 | self.uselist = False |
---|
1013 | |
---|
1014 | if self.uselist is None: |
---|
1015 | self.uselist = True |
---|
1016 | |
---|
1017 | if not self.viewonly: |
---|
1018 | self._dependency_processor = dependency.create_dependency_processor(self) |
---|
1019 | |
---|
1020 | # primary property handler, set up class attributes |
---|
1021 | if self.is_primary(): |
---|
1022 | if self.back_populates: |
---|
1023 | self.extension = list(util.to_list(self.extension, default=[])) |
---|
1024 | self.extension.append(attributes.GenericBackrefExtension(self.back_populates)) |
---|
1025 | self._add_reverse_property(self.back_populates) |
---|
1026 | |
---|
1027 | if self.backref is not None: |
---|
1028 | self.backref.compile(self) |
---|
1029 | elif not mapper.class_mapper(self.parent.class_, compile=False)._get_property(self.key, raiseerr=False): |
---|
1030 | raise sa_exc.ArgumentError("Attempting to assign a new relation '%s' to " |
---|
1031 | "a non-primary mapper on class '%s'. New relations can only be " |
---|
1032 | "added to the primary mapper, i.e. the very first " |
---|
1033 | "mapper created for class '%s' " % (self.key, self.parent.class_.__name__, self.parent.class_.__name__)) |
---|
1034 | |
---|
1035 | |
---|
1036 | def _refers_to_parent_table(self): |
---|
1037 | for c, f in self.synchronize_pairs: |
---|
1038 | if c.table is f.table: |
---|
1039 | return True |
---|
1040 | else: |
---|
1041 | return False |
---|
1042 | |
---|
1043 | def _is_self_referential(self): |
---|
1044 | return self.mapper.common_parent(self.parent) |
---|
1045 | |
---|
1046 | def _create_joins(self, source_polymorphic=False, source_selectable=None, dest_polymorphic=False, dest_selectable=None, of_type=None): |
---|
1047 | if source_selectable is None: |
---|
1048 | if source_polymorphic and self.parent.with_polymorphic: |
---|
1049 | source_selectable = self.parent._with_polymorphic_selectable |
---|
1050 | |
---|
1051 | aliased = False |
---|
1052 | if dest_selectable is None: |
---|
1053 | if dest_polymorphic and self.mapper.with_polymorphic: |
---|
1054 | dest_selectable = self.mapper._with_polymorphic_selectable |
---|
1055 | aliased = True |
---|
1056 | else: |
---|
1057 | dest_selectable = self.mapper.mapped_table |
---|
1058 | |
---|
1059 | if self._is_self_referential() and source_selectable is None: |
---|
1060 | dest_selectable = dest_selectable.alias() |
---|
1061 | aliased = True |
---|
1062 | else: |
---|
1063 | aliased = True |
---|
1064 | |
---|
1065 | aliased = aliased or bool(source_selectable) |
---|
1066 | |
---|
1067 | primaryjoin, secondaryjoin, secondary = self.primaryjoin, self.secondaryjoin, self.secondary |
---|
1068 | |
---|
1069 | # adjust the join condition for single table inheritance, |
---|
1070 | # in the case that the join is to a subclass |
---|
1071 | # this is analgous to the "_adjust_for_single_table_inheritance()" |
---|
1072 | # method in Query. |
---|
1073 | |
---|
1074 | dest_mapper = of_type or self.mapper |
---|
1075 | |
---|
1076 | single_crit = dest_mapper._single_table_criterion |
---|
1077 | if single_crit: |
---|
1078 | if secondaryjoin: |
---|
1079 | secondaryjoin = secondaryjoin & single_crit |
---|
1080 | else: |
---|
1081 | primaryjoin = primaryjoin & single_crit |
---|
1082 | |
---|
1083 | |
---|
1084 | if aliased: |
---|
1085 | if secondary: |
---|
1086 | secondary = secondary.alias() |
---|
1087 | primary_aliasizer = ClauseAdapter(secondary) |
---|
1088 | if dest_selectable: |
---|
1089 | secondary_aliasizer = ClauseAdapter(dest_selectable, equivalents=self.mapper._equivalent_columns).chain(primary_aliasizer) |
---|
1090 | else: |
---|
1091 | secondary_aliasizer = primary_aliasizer |
---|
1092 | |
---|
1093 | if source_selectable: |
---|
1094 | primary_aliasizer = ClauseAdapter(secondary).chain(ClauseAdapter(source_selectable, equivalents=self.parent._equivalent_columns)) |
---|
1095 | |
---|
1096 | secondaryjoin = secondary_aliasizer.traverse(secondaryjoin) |
---|
1097 | else: |
---|
1098 | if dest_selectable: |
---|
1099 | primary_aliasizer = ClauseAdapter(dest_selectable, exclude=self.local_side, equivalents=self.mapper._equivalent_columns) |
---|
1100 | if source_selectable: |
---|
1101 | primary_aliasizer.chain(ClauseAdapter(source_selectable, exclude=self.remote_side, equivalents=self.parent._equivalent_columns)) |
---|
1102 | elif source_selectable: |
---|
1103 | primary_aliasizer = ClauseAdapter(source_selectable, exclude=self.remote_side, equivalents=self.parent._equivalent_columns) |
---|
1104 | |
---|
1105 | secondary_aliasizer = None |
---|
1106 | |
---|
1107 | primaryjoin = primary_aliasizer.traverse(primaryjoin) |
---|
1108 | target_adapter = secondary_aliasizer or primary_aliasizer |
---|
1109 | target_adapter.include = target_adapter.exclude = None |
---|
1110 | else: |
---|
1111 | target_adapter = None |
---|
1112 | |
---|
1113 | return (primaryjoin, secondaryjoin, |
---|
1114 | (source_selectable or self.parent.local_table), |
---|
1115 | (dest_selectable or self.mapper.local_table), secondary, target_adapter) |
---|
1116 | |
---|
1117 | def _get_join(self, parent, primary=True, secondary=True, polymorphic_parent=True): |
---|
1118 | """deprecated. use primary_join_against(), secondary_join_against(), full_join_against()""" |
---|
1119 | |
---|
1120 | pj, sj, source, dest, secondarytable, adapter = self._create_joins(source_polymorphic=polymorphic_parent) |
---|
1121 | |
---|
1122 | if primary and secondary: |
---|
1123 | return pj & sj |
---|
1124 | elif primary: |
---|
1125 | return pj |
---|
1126 | elif secondary: |
---|
1127 | return sj |
---|
1128 | else: |
---|
1129 | raise AssertionError("illegal condition") |
---|
1130 | |
---|
1131 | def register_dependencies(self, uowcommit): |
---|
1132 | if not self.viewonly: |
---|
1133 | self._dependency_processor.register_dependencies(uowcommit) |
---|
1134 | |
---|
1135 | def register_processors(self, uowcommit): |
---|
1136 | if not self.viewonly: |
---|
1137 | self._dependency_processor.register_processors(uowcommit) |
---|
1138 | |
---|
1139 | PropertyLoader = RelationProperty |
---|
1140 | log.class_logger(RelationProperty) |
---|
1141 | |
---|
1142 | class BackRef(object): |
---|
1143 | """Attached to a RelationProperty to indicate a complementary reverse relationship. |
---|
1144 | |
---|
1145 | Handles the job of creating the opposite RelationProperty according to configuration. |
---|
1146 | |
---|
1147 | Alternatively, two explicit RelationProperty objects can be associated bidirectionally |
---|
1148 | using the back_populates keyword argument on each. |
---|
1149 | |
---|
1150 | """ |
---|
1151 | |
---|
1152 | def __init__(self, key, _prop=None, **kwargs): |
---|
1153 | self.key = key |
---|
1154 | self.kwargs = kwargs |
---|
1155 | self.prop = _prop |
---|
1156 | self.extension = attributes.GenericBackrefExtension(self.key) |
---|
1157 | |
---|
1158 | def compile(self, prop): |
---|
1159 | if self.prop: |
---|
1160 | return |
---|
1161 | |
---|
1162 | self.prop = prop |
---|
1163 | |
---|
1164 | mapper = prop.mapper.primary_mapper() |
---|
1165 | if mapper._get_property(self.key, raiseerr=False) is None: |
---|
1166 | if prop.secondary: |
---|
1167 | pj = self.kwargs.pop('primaryjoin', prop.secondaryjoin) |
---|
1168 | sj = self.kwargs.pop('secondaryjoin', prop.primaryjoin) |
---|
1169 | else: |
---|
1170 | pj = self.kwargs.pop('primaryjoin', prop.primaryjoin) |
---|
1171 | sj = self.kwargs.pop('secondaryjoin', None) |
---|
1172 | if sj: |
---|
1173 | raise sa_exc.InvalidRequestError( |
---|
1174 | "Can't assign 'secondaryjoin' on a backref against " |
---|
1175 | "a non-secondary relation.") |
---|
1176 | |
---|
1177 | foreign_keys = self.kwargs.pop('foreign_keys', prop._foreign_keys) |
---|
1178 | |
---|
1179 | parent = prop.parent.primary_mapper() |
---|
1180 | self.kwargs.setdefault('viewonly', prop.viewonly) |
---|
1181 | self.kwargs.setdefault('post_update', prop.post_update) |
---|
1182 | |
---|
1183 | relation = RelationProperty(parent, prop.secondary, pj, sj, foreign_keys=foreign_keys, |
---|
1184 | backref=BackRef(prop.key, _prop=prop), |
---|
1185 | **self.kwargs) |
---|
1186 | |
---|
1187 | mapper._configure_property(self.key, relation); |
---|
1188 | |
---|
1189 | prop._add_reverse_property(self.key) |
---|
1190 | |
---|
1191 | else: |
---|
1192 | raise sa_exc.ArgumentError("Error creating backref '%s' on relation '%s': " |
---|
1193 | "property of that name exists on mapper '%s'" % (self.key, prop, mapper)) |
---|
1194 | |
---|
1195 | mapper.ColumnProperty = ColumnProperty |
---|
1196 | mapper.SynonymProperty = SynonymProperty |
---|
1197 | mapper.ComparableProperty = ComparableProperty |
---|
1198 | mapper.RelationProperty = RelationProperty |
---|
1199 | mapper.ConcreteInheritedProperty = ConcreteInheritedProperty |
---|