1# orm/strategy_options.py
2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""
9
10"""
11
12from . import util as orm_util
13from .attributes import QueryableAttribute
14from .base import _class_to_mapper
15from .base import _is_aliased_class
16from .base import _is_mapped_class
17from .base import InspectionAttr
18from .interfaces import LoaderOption
19from .interfaces import MapperProperty
20from .interfaces import PropComparator
21from .path_registry import _DEFAULT_TOKEN
22from .path_registry import _WILDCARD_TOKEN
23from .path_registry import PathRegistry
24from .path_registry import TokenRegistry
25from .util import _orm_full_deannotate
26from .. import exc as sa_exc
27from .. import inspect
28from .. import util
29from ..sql import and_
30from ..sql import coercions
31from ..sql import roles
32from ..sql import traversals
33from ..sql import visitors
34from ..sql.base import _generative
35from ..sql.base import Generative
36
37
38class Load(Generative, LoaderOption):
39 """Represents loader options which modify the state of a
40 :class:`_query.Query` in order to affect how various mapped attributes are
41 loaded.
42
43 The :class:`_orm.Load` object is in most cases used implicitly behind the
44 scenes when one makes use of a query option like :func:`_orm.joinedload`,
45 :func:`.defer`, or similar. However, the :class:`_orm.Load` object
46 can also be used directly, and in some cases can be useful.
47
48 To use :class:`_orm.Load` directly, instantiate it with the target mapped
49 class as the argument. This style of usage is
50 useful when dealing with a :class:`_query.Query`
51 that has multiple entities::
52
53 myopt = Load(MyClass).joinedload("widgets")
54
55 The above ``myopt`` can now be used with :meth:`_query.Query.options`,
56 where it
57 will only take effect for the ``MyClass`` entity::
58
59 session.query(MyClass, MyOtherClass).options(myopt)
60
61 One case where :class:`_orm.Load`
62 is useful as public API is when specifying
63 "wildcard" options that only take effect for a certain class::
64
65 session.query(Order).options(Load(Order).lazyload('*'))
66
67 Above, all relationships on ``Order`` will be lazy-loaded, but other
68 attributes on those descendant objects will load using their normal
69 loader strategy.
70
71 .. seealso::
72
73 :ref:`deferred_options`
74
75 :ref:`deferred_loading_w_multiple`
76
77 :ref:`relationship_loader_options`
78
79 """
80
81 _is_strategy_option = True
82
83 _cache_key_traversal = [
84 ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
85 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
86 ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
87 ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
88 (
89 "_context_cache_key",
90 visitors.ExtendedInternalTraversal.dp_has_cache_key_tuples,
91 ),
92 (
93 "local_opts",
94 visitors.ExtendedInternalTraversal.dp_string_multi_dict,
95 ),
96 ]
97
98 def __init__(self, entity):
99 insp = inspect(entity)
100 insp._post_inspect
101
102 self.path = insp._path_registry
103 # note that this .context is shared among all descendant
104 # Load objects
105 self.context = util.OrderedDict()
106 self.local_opts = {}
107 self.is_class_strategy = False
108
109 @classmethod
110 def for_existing_path(cls, path):
111 load = cls.__new__(cls)
112 load.path = path
113 load.context = {}
114 load.local_opts = {}
115 load._of_type = None
116 load._extra_criteria = ()
117 return load
118
119 def _adapt_cached_option_to_uncached_option(self, context, uncached_opt):
120 return self._adjust_for_extra_criteria(context)
121
122 def _generate_extra_criteria(self, context):
123 """Apply the current bound parameters in a QueryContext to the
124 immediate "extra_criteria" stored with this Load object.
125
126 Load objects are typically pulled from the cached version of
127 the statement from a QueryContext. The statement currently being
128 executed will have new values (and keys) for bound parameters in the
129 extra criteria which need to be applied by loader strategies when
130 they handle this criteria for a result set.
131
132 """
133
134 assert (
135 self._extra_criteria
136 ), "this should only be called if _extra_criteria is present"
137
138 orig_query = context.compile_state.select_statement
139 current_query = context.query
140
141 # NOTE: while it seems like we should not do the "apply" operation
142 # here if orig_query is current_query, skipping it in the "optimized"
143 # case causes the query to be different from a cache key perspective,
144 # because we are creating a copy of the criteria which is no longer
145 # the same identity of the _extra_criteria in the loader option
146 # itself. cache key logic produces a different key for
147 # (A, copy_of_A) vs. (A, A), because in the latter case it shortens
148 # the second part of the key to just indicate on identity.
149
150 # if orig_query is current_query:
151 # not cached yet. just do the and_()
152 # return and_(*self._extra_criteria)
153
154 k1 = orig_query._generate_cache_key()
155 k2 = current_query._generate_cache_key()
156
157 return k2._apply_params_to_element(k1, and_(*self._extra_criteria))
158
159 def _adjust_for_extra_criteria(self, context):
160 """Apply the current bound parameters in a QueryContext to all
161 occurrences "extra_criteria" stored within al this Load object;
162 copying in place.
163
164 """
165 orig_query = context.compile_state.select_statement
166
167 applied = {}
168
169 ck = [None, None]
170
171 def process(opt):
172 if not opt._extra_criteria:
173 return
174
175 if ck[0] is None:
176 ck[:] = (
177 orig_query._generate_cache_key(),
178 context.query._generate_cache_key(),
179 )
180 k1, k2 = ck
181
182 opt._extra_criteria = tuple(
183 k2._apply_params_to_element(k1, crit)
184 for crit in opt._extra_criteria
185 )
186
187 return self._deep_clone(applied, process)
188
189 def _deep_clone(self, applied, process):
190 if self in applied:
191 return applied[self]
192
193 cloned = self._generate()
194
195 applied[self] = cloned
196
197 cloned.strategy = self.strategy
198
199 assert cloned.propagate_to_loaders == self.propagate_to_loaders
200 assert cloned.is_class_strategy == self.is_class_strategy
201 assert cloned.is_opts_only == self.is_opts_only
202
203 if self.context:
204 cloned.context = util.OrderedDict(
205 [
206 (
207 key,
208 value._deep_clone(applied, process)
209 if isinstance(value, Load)
210 else value,
211 )
212 for key, value in self.context.items()
213 ]
214 )
215
216 cloned.local_opts.update(self.local_opts)
217
218 process(cloned)
219
220 return cloned
221
222 @property
223 def _context_cache_key(self):
224 serialized = []
225 if self.context is None:
226 return []
227 for (key, loader_path), obj in self.context.items():
228 if key != "loader":
229 continue
230 serialized.append(loader_path + (obj,))
231 return serialized
232
233 def _generate(self):
234 cloned = super(Load, self)._generate()
235 cloned.local_opts = {}
236 return cloned
237
238 is_opts_only = False
239 is_class_strategy = False
240 strategy = None
241 propagate_to_loaders = False
242 _of_type = None
243 _extra_criteria = ()
244
245 def process_compile_state_replaced_entities(
246 self, compile_state, mapper_entities
247 ):
248 if not compile_state.compile_options._enable_eagerloads:
249 return
250
251 # process is being run here so that the options given are validated
252 # against what the lead entities were, as well as to accommodate
253 # for the entities having been replaced with equivalents
254 self._process(
255 compile_state,
256 mapper_entities,
257 not bool(compile_state.current_path),
258 )
259
260 def process_compile_state(self, compile_state):
261 if not compile_state.compile_options._enable_eagerloads:
262 return
263
264 self._process(
265 compile_state,
266 compile_state._lead_mapper_entities,
267 not bool(compile_state.current_path)
268 and not compile_state.compile_options._for_refresh_state,
269 )
270
271 def _process(self, compile_state, mapper_entities, raiseerr):
272 is_refresh = compile_state.compile_options._for_refresh_state
273 current_path = compile_state.current_path
274 if current_path:
275 for (token, start_path), loader in self.context.items():
276 if is_refresh and not loader.propagate_to_loaders:
277 continue
278 chopped_start_path = self._chop_path(start_path, current_path)
279 if chopped_start_path is not None:
280 compile_state.attributes[
281 (token, chopped_start_path)
282 ] = loader
283 else:
284 compile_state.attributes.update(self.context)
285
286 def _generate_path(
287 self,
288 path,
289 attr,
290 for_strategy,
291 wildcard_key,
292 raiseerr=True,
293 polymorphic_entity_context=None,
294 ):
295 existing_of_type = self._of_type
296 self._of_type = None
297 if raiseerr and not path.has_entity:
298 if isinstance(path, TokenRegistry):
299 raise sa_exc.ArgumentError(
300 "Wildcard token cannot be followed by another entity"
301 )
302 else:
303 raise sa_exc.ArgumentError(
304 "Mapped attribute '%s' does not "
305 "refer to a mapped entity" % (path.prop,)
306 )
307
308 if isinstance(attr, util.string_types):
309
310 default_token = attr.endswith(_DEFAULT_TOKEN)
311 attr_str_name = attr
312 if attr.endswith(_WILDCARD_TOKEN) or default_token:
313 if default_token:
314 self.propagate_to_loaders = False
315 if wildcard_key:
316 attr = "%s:%s" % (wildcard_key, attr)
317
318 # TODO: AliasedInsp inside the path for of_type is not
319 # working for a with_polymorphic entity because the
320 # relationship loaders don't render the with_poly into the
321 # path. See #4469 which will try to improve this
322 if existing_of_type and not existing_of_type.is_aliased_class:
323 path = path.parent[existing_of_type]
324 path = path.token(attr)
325 self.path = path
326 return path
327
328 if existing_of_type:
329 ent = inspect(existing_of_type)
330 else:
331 ent = path.entity
332
333 util.warn_deprecated_20(
334 "Using strings to indicate column or "
335 "relationship paths in loader options is deprecated "
336 "and will be removed in SQLAlchemy 2.0. Please use "
337 "the class-bound attribute directly.",
338 )
339 try:
340 # use getattr on the class to work around
341 # synonyms, hybrids, etc.
342 attr = getattr(ent.class_, attr)
343 except AttributeError as err:
344 if raiseerr:
345 util.raise_(
346 sa_exc.ArgumentError(
347 'Can\'t find property named "%s" on '
348 "%s in this Query." % (attr, ent)
349 ),
350 replace_context=err,
351 )
352 else:
353 return None
354 else:
355 try:
356 attr = found_property = attr.property
357 except AttributeError as ae:
358 if not isinstance(attr, MapperProperty):
359 util.raise_(
360 sa_exc.ArgumentError(
361 'Expected attribute "%s" on %s to be a '
362 "mapped attribute; "
363 "instead got %s object."
364 % (attr_str_name, ent, type(attr))
365 ),
366 replace_context=ae,
367 )
368 else:
369 raise
370
371 path = path[attr]
372 else:
373 insp = inspect(attr)
374
375 if insp.is_mapper or insp.is_aliased_class:
376 # TODO: this does not appear to be a valid codepath. "attr"
377 # would never be a mapper. This block is present in 1.2
378 # as well however does not seem to be accessed in any tests.
379 if not orm_util._entity_corresponds_to_use_path_impl(
380 attr.parent, path[-1]
381 ):
382 if raiseerr:
383 raise sa_exc.ArgumentError(
384 "Attribute '%s' does not "
385 "link from element '%s'" % (attr, path.entity)
386 )
387 else:
388 return None
389 elif insp.is_property:
390 prop = found_property = attr
391 path = path[prop]
392 elif insp.is_attribute:
393 prop = found_property = attr.property
394
395 if not orm_util._entity_corresponds_to_use_path_impl(
396 attr.parent, path[-1]
397 ):
398 if raiseerr:
399 raise sa_exc.ArgumentError(
400 'Attribute "%s" does not '
401 'link from element "%s".%s'
402 % (
403 attr,
404 path.entity,
405 (
406 " Did you mean to use "
407 "%s.of_type(%s)?"
408 % (path[-2], attr.class_.__name__)
409 if len(path) > 1
410 and path.entity.is_mapper
411 and attr.parent.is_aliased_class
412 else ""
413 ),
414 )
415 )
416 else:
417 return None
418
419 if attr._extra_criteria and not self._extra_criteria:
420 # in most cases, the process that brings us here will have
421 # already established _extra_criteria. however if not,
422 # and it's present on the attribute, then use that.
423 self._extra_criteria = attr._extra_criteria
424
425 if getattr(attr, "_of_type", None):
426 ac = attr._of_type
427 ext_info = of_type_info = inspect(ac)
428
429 if polymorphic_entity_context is None:
430 polymorphic_entity_context = self.context
431
432 existing = path.entity_path[prop].get(
433 polymorphic_entity_context, "path_with_polymorphic"
434 )
435
436 if not ext_info.is_aliased_class:
437 ac = orm_util.with_polymorphic(
438 ext_info.mapper.base_mapper,
439 ext_info.mapper,
440 aliased=True,
441 _use_mapper_path=True,
442 _existing_alias=inspect(existing)
443 if existing is not None
444 else None,
445 )
446
447 ext_info = inspect(ac)
448
449 path.entity_path[prop].set(
450 polymorphic_entity_context, "path_with_polymorphic", ac
451 )
452
453 path = path[prop][ext_info]
454
455 self._of_type = of_type_info
456
457 else:
458 path = path[prop]
459
460 if for_strategy is not None:
461 found_property._get_strategy(for_strategy)
462 if path.has_entity:
463 path = path.entity_path
464 self.path = path
465 return path
466
467 def __str__(self):
468 return "Load(strategy=%r)" % (self.strategy,)
469
470 def _coerce_strat(self, strategy):
471 if strategy is not None:
472 strategy = tuple(sorted(strategy.items()))
473 return strategy
474
475 def _apply_to_parent(self, parent, applied, bound):
476 raise NotImplementedError(
477 "Only 'unbound' loader options may be used with the "
478 "Load.options() method"
479 )
480
481 @_generative
482 def options(self, *opts):
483 r"""Apply a series of options as sub-options to this
484 :class:`_orm.Load`
485 object.
486
487 E.g.::
488
489 query = session.query(Author)
490 query = query.options(
491 joinedload(Author.book).options(
492 load_only(Book.summary, Book.excerpt),
493 joinedload(Book.citations).options(
494 joinedload(Citation.author)
495 )
496 )
497 )
498
499 :param \*opts: A series of loader option objects (ultimately
500 :class:`_orm.Load` objects) which should be applied to the path
501 specified by this :class:`_orm.Load` object.
502
503 .. versionadded:: 1.3.6
504
505 .. seealso::
506
507 :func:`.defaultload`
508
509 :ref:`relationship_loader_options`
510
511 :ref:`deferred_loading_w_multiple`
512
513 """
514 apply_cache = {}
515 bound = not isinstance(self, _UnboundLoad)
516 if bound:
517 raise NotImplementedError(
518 "The options() method is currently only supported "
519 "for 'unbound' loader options"
520 )
521 for opt in opts:
522 try:
523 opt._apply_to_parent(self, apply_cache, bound)
524 except AttributeError as ae:
525 if not isinstance(opt, Load):
526 util.raise_(
527 sa_exc.ArgumentError(
528 "Loader option %s is not compatible with the "
529 "Load.options() method." % (opt,)
530 ),
531 from_=ae,
532 )
533 else:
534 raise
535
536 @_generative
537 def set_relationship_strategy(
538 self, attr, strategy, propagate_to_loaders=True
539 ):
540 strategy = self._coerce_strat(strategy)
541 self.propagate_to_loaders = propagate_to_loaders
542 cloned = self._clone_for_bind_strategy(attr, strategy, "relationship")
543 self.path = cloned.path
544 self._of_type = cloned._of_type
545 self._extra_criteria = cloned._extra_criteria
546 cloned.is_class_strategy = self.is_class_strategy = False
547 self.propagate_to_loaders = cloned.propagate_to_loaders
548
549 @_generative
550 def set_column_strategy(self, attrs, strategy, opts=None, opts_only=False):
551 strategy = self._coerce_strat(strategy)
552 self.is_class_strategy = False
553 for attr in attrs:
554 cloned = self._clone_for_bind_strategy(
555 attr, strategy, "column", opts_only=opts_only, opts=opts
556 )
557 cloned.propagate_to_loaders = True
558
559 @_generative
560 def set_generic_strategy(self, attrs, strategy):
561 strategy = self._coerce_strat(strategy)
562 for attr in attrs:
563 cloned = self._clone_for_bind_strategy(attr, strategy, None)
564 cloned.propagate_to_loaders = True
565
566 @_generative
567 def set_class_strategy(self, strategy, opts):
568 strategy = self._coerce_strat(strategy)
569 cloned = self._clone_for_bind_strategy(None, strategy, None)
570 cloned.is_class_strategy = True
571 cloned.propagate_to_loaders = True
572 cloned.local_opts.update(opts)
573
574 def _clone_for_bind_strategy(
575 self, attr, strategy, wildcard_key, opts_only=False, opts=None
576 ):
577 """Create an anonymous clone of the Load/_UnboundLoad that is suitable
578 to be placed in the context / _to_bind collection of this Load
579 object. The clone will then lose references to context/_to_bind
580 in order to not create reference cycles.
581
582 """
583 cloned = self._generate()
584 cloned._generate_path(self.path, attr, strategy, wildcard_key)
585 cloned.strategy = strategy
586
587 cloned.local_opts = self.local_opts
588 if opts:
589 cloned.local_opts.update(opts)
590 if opts_only:
591 cloned.is_opts_only = True
592
593 if strategy or cloned.is_opts_only:
594 cloned._set_path_strategy()
595 return cloned
596
597 def _set_for_path(self, context, path, replace=True, merge_opts=False):
598 if merge_opts or not replace:
599 existing = path.get(context, "loader")
600 if existing:
601 if merge_opts:
602 existing.local_opts.update(self.local_opts)
603 existing._extra_criteria += self._extra_criteria
604 else:
605 path.set(context, "loader", self)
606 else:
607 existing = path.get(context, "loader")
608 path.set(context, "loader", self)
609 if existing and existing.is_opts_only:
610 self.local_opts.update(existing.local_opts)
611 existing._extra_criteria += self._extra_criteria
612
613 def _set_path_strategy(self):
614 if not self.is_class_strategy and self.path.has_entity:
615 effective_path = self.path.parent
616 else:
617 effective_path = self.path
618
619 if effective_path.is_token:
620 for path in effective_path.generate_for_superclasses():
621 self._set_for_path(
622 self.context,
623 path,
624 replace=True,
625 merge_opts=self.is_opts_only,
626 )
627 else:
628 self._set_for_path(
629 self.context,
630 effective_path,
631 replace=True,
632 merge_opts=self.is_opts_only,
633 )
634
635 # remove cycles; _set_path_strategy is always invoked on an
636 # anonymous clone of the Load / UnboundLoad object since #5056
637 self.context = None
638
639 def __getstate__(self):
640 d = self.__dict__.copy()
641
642 # can't pickle this right now; warning is raised by strategies
643 d["_extra_criteria"] = ()
644
645 if d["context"] is not None:
646 d["context"] = PathRegistry.serialize_context_dict(
647 d["context"], ("loader",)
648 )
649 d["path"] = self.path.serialize()
650 return d
651
652 def __setstate__(self, state):
653 self.__dict__.update(state)
654 self.path = PathRegistry.deserialize(self.path)
655 if self.context is not None:
656 self.context = PathRegistry.deserialize_context_dict(self.context)
657
658 def _chop_path(self, to_chop, path):
659 i = -1
660
661 for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)):
662 if isinstance(c_token, util.string_types):
663 # TODO: this is approximated from the _UnboundLoad
664 # version and probably has issues, not fully covered.
665
666 if i == 0 and c_token.endswith(":" + _DEFAULT_TOKEN):
667 return to_chop
668 elif (
669 c_token != "relationship:%s" % (_WILDCARD_TOKEN,)
670 and c_token != p_token.key
671 ):
672 return None
673
674 if c_token is p_token:
675 continue
676 elif (
677 isinstance(c_token, InspectionAttr)
678 and c_token.is_mapper
679 and p_token.is_mapper
680 and c_token.isa(p_token)
681 ):
682 continue
683 else:
684 return None
685 return to_chop[i + 1 :]
686
687
688class _UnboundLoad(Load):
689 """Represent a loader option that isn't tied to a root entity.
690
691 The loader option will produce an entity-linked :class:`_orm.Load`
692 object when it is passed :meth:`_query.Query.options`.
693
694 This provides compatibility with the traditional system
695 of freestanding options, e.g. ``joinedload('x.y.z')``.
696
697 """
698
699 def __init__(self):
700 self.path = ()
701 self._to_bind = []
702 self.local_opts = {}
703 self._extra_criteria = ()
704
705 def _gen_cache_key(self, anon_map, bindparams, _unbound_option_seen=None):
706 """Inlined gen_cache_key
707
708 Original traversal is::
709
710
711 _cache_key_traversal = [
712 ("path", visitors.ExtendedInternalTraversal.dp_multi_list),
713 ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
714 (
715 "_to_bind",
716 visitors.ExtendedInternalTraversal.dp_has_cache_key_list,
717 ),
718 (
719 "_extra_criteria",
720 visitors.InternalTraversal.dp_clauseelement_list),
721 (
722 "local_opts",
723 visitors.ExtendedInternalTraversal.dp_string_multi_dict,
724 ),
725 ]
726
727 The inlining is so that the "_to_bind" list can be flattened to not
728 repeat the same UnboundLoad options over and over again.
729
730 See #6869
731
732 """
733
734 idself = id(self)
735 cls = self.__class__
736
737 if idself in anon_map:
738 return (anon_map[idself], cls)
739 else:
740 id_ = anon_map[idself]
741
742 vis = traversals._cache_key_traversal_visitor
743
744 seen = _unbound_option_seen
745 if seen is None:
746 seen = set()
747
748 return (
749 (id_, cls)
750 + vis.visit_multi_list(
751 "path", self.path, self, anon_map, bindparams
752 )
753 + ("strategy", self.strategy)
754 + (
755 (
756 "_to_bind",
757 tuple(
758 elem._gen_cache_key(
759 anon_map, bindparams, _unbound_option_seen=seen
760 )
761 for elem in self._to_bind
762 if elem not in seen and not seen.add(elem)
763 ),
764 )
765 if self._to_bind
766 else ()
767 )
768 + (
769 (
770 "_extra_criteria",
771 tuple(
772 elem._gen_cache_key(anon_map, bindparams)
773 for elem in self._extra_criteria
774 ),
775 )
776 if self._extra_criteria
777 else ()
778 )
779 + (
780 vis.visit_string_multi_dict(
781 "local_opts", self.local_opts, self, anon_map, bindparams
782 )
783 if self.local_opts
784 else ()
785 )
786 )
787
788 _is_chain_link = False
789
790 def _set_path_strategy(self):
791 self._to_bind.append(self)
792
793 # remove cycles; _set_path_strategy is always invoked on an
794 # anonymous clone of the Load / UnboundLoad object since #5056
795 self._to_bind = None
796
797 def _deep_clone(self, applied, process):
798 if self in applied:
799 return applied[self]
800
801 cloned = self._generate()
802
803 applied[self] = cloned
804
805 cloned.strategy = self.strategy
806
807 assert cloned.propagate_to_loaders == self.propagate_to_loaders
808 assert cloned.is_class_strategy == self.is_class_strategy
809 assert cloned.is_opts_only == self.is_opts_only
810
811 cloned._to_bind = [
812 elem._deep_clone(applied, process) for elem in self._to_bind or ()
813 ]
814
815 cloned.local_opts.update(self.local_opts)
816
817 process(cloned)
818
819 return cloned
820
821 def _apply_to_parent(self, parent, applied, bound, to_bind=None):
822 if self in applied:
823 return applied[self]
824
825 if to_bind is None:
826 to_bind = self._to_bind
827
828 cloned = self._generate()
829
830 applied[self] = cloned
831
832 cloned.strategy = self.strategy
833 if self.path:
834 attr = self.path[-1]
835 if isinstance(attr, util.string_types) and attr.endswith(
836 _DEFAULT_TOKEN
837 ):
838 attr = attr.split(":")[0] + ":" + _WILDCARD_TOKEN
839 cloned._generate_path(
840 parent.path + self.path[0:-1], attr, self.strategy, None
841 )
842
843 # these assertions can go away once the "sub options" API is
844 # mature
845 assert cloned.propagate_to_loaders == self.propagate_to_loaders
846 assert cloned.is_class_strategy == self.is_class_strategy
847 assert cloned.is_opts_only == self.is_opts_only
848
849 uniq = set()
850
851 cloned._to_bind = parent._to_bind
852
853 cloned._to_bind[:] = [
854 elem
855 for elem in cloned._to_bind
856 if elem not in uniq and not uniq.add(elem)
857 ] + [
858 elem._apply_to_parent(parent, applied, bound, to_bind)
859 for elem in to_bind
860 if elem not in uniq and not uniq.add(elem)
861 ]
862
863 cloned.local_opts.update(self.local_opts)
864
865 return cloned
866
867 def _generate_path(self, path, attr, for_strategy, wildcard_key):
868 if (
869 wildcard_key
870 and isinstance(attr, util.string_types)
871 and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN)
872 ):
873 if attr == _DEFAULT_TOKEN:
874 self.propagate_to_loaders = False
875 attr = "%s:%s" % (wildcard_key, attr)
876 if path and _is_mapped_class(path[-1]) and not self.is_class_strategy:
877 path = path[0:-1]
878 if attr:
879 path = path + (attr,)
880 self.path = path
881 self._extra_criteria = getattr(attr, "_extra_criteria", ())
882
883 return path
884
885 def __getstate__(self):
886 d = self.__dict__.copy()
887
888 # can't pickle this right now; warning is raised by strategies
889 d["_extra_criteria"] = ()
890
891 d["path"] = self._serialize_path(self.path, filter_aliased_class=True)
892 return d
893
894 def __setstate__(self, state):
895 ret = []
896 for key in state["path"]:
897 if isinstance(key, tuple):
898 if len(key) == 2:
899 # support legacy
900 cls, propkey = key
901 of_type = None
902 else:
903 cls, propkey, of_type = key
904 prop = getattr(cls, propkey)
905 if of_type:
906 prop = prop.of_type(of_type)
907 ret.append(prop)
908 else:
909 ret.append(key)
910 state["path"] = tuple(ret)
911 self.__dict__ = state
912
913 def _process(self, compile_state, mapper_entities, raiseerr):
914 dedupes = compile_state.attributes["_unbound_load_dedupes"]
915 is_refresh = compile_state.compile_options._for_refresh_state
916 for val in self._to_bind:
917 if val not in dedupes:
918 dedupes.add(val)
919 if is_refresh and not val.propagate_to_loaders:
920 continue
921 val._bind_loader(
922 [ent.entity_zero for ent in mapper_entities],
923 compile_state.current_path,
924 compile_state.attributes,
925 raiseerr,
926 )
927
928 @classmethod
929 def _from_keys(cls, meth, keys, chained, kw):
930 opt = _UnboundLoad()
931
932 def _split_key(key):
933 if isinstance(key, util.string_types):
934 # coerce fooload('*') into "default loader strategy"
935 if key == _WILDCARD_TOKEN:
936 return (_DEFAULT_TOKEN,)
937 # coerce fooload(".*") into "wildcard on default entity"
938 elif key.startswith("." + _WILDCARD_TOKEN):
939 util.warn_deprecated(
940 "The undocumented `.{WILDCARD}` format is deprecated "
941 "and will be removed in a future version as it is "
942 "believed to be unused. "
943 "If you have been using this functionality, please "
944 "comment on Issue #4390 on the SQLAlchemy project "
945 "tracker.",
946 version="1.4",
947 )
948 key = key[1:]
949 return key.split(".")
950 else:
951 return (key,)
952
953 all_tokens = [token for key in keys for token in _split_key(key)]
954
955 for token in all_tokens[0:-1]:
956 # set _is_chain_link first so that clones of the
957 # object also inherit this flag
958 opt._is_chain_link = True
959 if chained:
960 opt = meth(opt, token, **kw)
961 else:
962 opt = opt.defaultload(token)
963
964 opt = meth(opt, all_tokens[-1], **kw)
965 opt._is_chain_link = False
966 return opt
967
968 def _chop_path(self, to_chop, path):
969 i = -1
970 for i, (c_token, (p_entity, p_prop)) in enumerate(
971 zip(to_chop, path.pairs())
972 ):
973 if isinstance(c_token, util.string_types):
974 if i == 0 and c_token.endswith(":" + _DEFAULT_TOKEN):
975 return to_chop
976 elif (
977 c_token != "relationship:%s" % (_WILDCARD_TOKEN,)
978 and c_token != p_prop.key
979 ):
980 return None
981 elif isinstance(c_token, PropComparator):
982 if c_token.property is not p_prop or (
983 c_token._parententity is not p_entity
984 and (
985 not c_token._parententity.is_mapper
986 or not c_token._parententity.isa(p_entity)
987 )
988 ):
989 return None
990 else:
991 i += 1
992
993 return to_chop[i:]
994
995 def _serialize_path(self, path, filter_aliased_class=False):
996 ret = []
997 for token in path:
998 if isinstance(token, QueryableAttribute):
999 if (
1000 filter_aliased_class
1001 and token._of_type
1002 and inspect(token._of_type).is_aliased_class
1003 ):
1004 ret.append((token._parentmapper.class_, token.key, None))
1005 else:
1006 ret.append(
1007 (
1008 token._parentmapper.class_,
1009 token.key,
1010 token._of_type.entity if token._of_type else None,
1011 )
1012 )
1013 elif isinstance(token, PropComparator):
1014 ret.append((token._parentmapper.class_, token.key, None))
1015 else:
1016 ret.append(token)
1017 return ret
1018
1019 def _bind_loader(self, entities, current_path, context, raiseerr):
1020 """Convert from an _UnboundLoad() object into a Load() object.
1021
1022 The _UnboundLoad() uses an informal "path" and does not necessarily
1023 refer to a lead entity as it may use string tokens. The Load()
1024 OTOH refers to a complete path. This method reconciles from a
1025 given Query into a Load.
1026
1027 Example::
1028
1029
1030 query = session.query(User).options(
1031 joinedload("orders").joinedload("items"))
1032
1033 The above options will be an _UnboundLoad object along the lines
1034 of (note this is not the exact API of _UnboundLoad)::
1035
1036 _UnboundLoad(
1037 _to_bind=[
1038 _UnboundLoad(["orders"], {"lazy": "joined"}),
1039 _UnboundLoad(["orders", "items"], {"lazy": "joined"}),
1040 ]
1041 )
1042
1043 After this method, we get something more like this (again this is
1044 not exact API)::
1045
1046 Load(
1047 User,
1048 (User, User.orders.property))
1049 Load(
1050 User,
1051 (User, User.orders.property, Order, Order.items.property))
1052
1053 """
1054
1055 start_path = self.path
1056
1057 if self.is_class_strategy and current_path:
1058 start_path += (entities[0],)
1059
1060 # _current_path implies we're in a
1061 # secondary load with an existing path
1062
1063 if current_path:
1064 start_path = self._chop_path(start_path, current_path)
1065
1066 if not start_path:
1067 return None
1068
1069 # look at the first token and try to locate within the Query
1070 # what entity we are referring towards.
1071 token = start_path[0]
1072
1073 if isinstance(token, util.string_types):
1074 entity = self._find_entity_basestring(entities, token, raiseerr)
1075 elif isinstance(token, PropComparator):
1076 prop = token.property
1077 entity = self._find_entity_prop_comparator(
1078 entities, prop, token._parententity, raiseerr
1079 )
1080 elif self.is_class_strategy and _is_mapped_class(token):
1081 entity = inspect(token)
1082 if entity not in entities:
1083 entity = None
1084 else:
1085 raise sa_exc.ArgumentError(
1086 "mapper option expects " "string key or list of attributes"
1087 )
1088
1089 if not entity:
1090 return
1091
1092 path_element = entity
1093
1094 # transfer our entity-less state into a Load() object
1095 # with a real entity path. Start with the lead entity
1096 # we just located, then go through the rest of our path
1097 # tokens and populate into the Load().
1098 loader = Load(path_element)
1099
1100 if context is None:
1101 context = loader.context
1102
1103 loader.strategy = self.strategy
1104 loader.is_opts_only = self.is_opts_only
1105 loader.is_class_strategy = self.is_class_strategy
1106 loader._extra_criteria = self._extra_criteria
1107
1108 path = loader.path
1109
1110 if not loader.is_class_strategy:
1111 for idx, token in enumerate(start_path):
1112 if not loader._generate_path(
1113 loader.path,
1114 token,
1115 self.strategy if idx == len(start_path) - 1 else None,
1116 None,
1117 raiseerr,
1118 polymorphic_entity_context=context,
1119 ):
1120 return
1121
1122 loader.local_opts.update(self.local_opts)
1123
1124 if not loader.is_class_strategy and loader.path.has_entity:
1125 effective_path = loader.path.parent
1126 else:
1127 effective_path = loader.path
1128
1129 # prioritize "first class" options over those
1130 # that were "links in the chain", e.g. "x" and "y" in
1131 # someload("x.y.z") versus someload("x") / someload("x.y")
1132
1133 if effective_path.is_token:
1134 for path in effective_path.generate_for_superclasses():
1135 loader._set_for_path(
1136 context,
1137 path,
1138 replace=not self._is_chain_link,
1139 merge_opts=self.is_opts_only,
1140 )
1141 else:
1142 loader._set_for_path(
1143 context,
1144 effective_path,
1145 replace=not self._is_chain_link,
1146 merge_opts=self.is_opts_only,
1147 )
1148
1149 return loader
1150
1151 def _find_entity_prop_comparator(self, entities, prop, mapper, raiseerr):
1152 if _is_aliased_class(mapper):
1153 searchfor = mapper
1154 else:
1155 searchfor = _class_to_mapper(mapper)
1156 for ent in entities:
1157 if orm_util._entity_corresponds_to(ent, searchfor):
1158 return ent
1159 else:
1160 if raiseerr:
1161 if not list(entities):
1162 raise sa_exc.ArgumentError(
1163 "Query has only expression-based entities, "
1164 'which do not apply to %s "%s"'
1165 % (util.clsname_as_plain_name(type(prop)), prop)
1166 )
1167 else:
1168 raise sa_exc.ArgumentError(
1169 'Mapped attribute "%s" does not apply to any of the '
1170 "root entities in this query, e.g. %s. Please "
1171 "specify the full path "
1172 "from one of the root entities to the target "
1173 "attribute. "
1174 % (prop, ", ".join(str(x) for x in entities))
1175 )
1176 else:
1177 return None
1178
1179 def _find_entity_basestring(self, entities, token, raiseerr):
1180 if token.endswith(":" + _WILDCARD_TOKEN):
1181 if len(list(entities)) != 1:
1182 if raiseerr:
1183 raise sa_exc.ArgumentError(
1184 "Can't apply wildcard ('*') or load_only() "
1185 "loader option to multiple entities %s. Specify "
1186 "loader options for each entity individually, such "
1187 "as %s."
1188 % (
1189 ", ".join(str(ent) for ent in entities),
1190 ", ".join(
1191 "Load(%s).some_option('*')" % ent
1192 for ent in entities
1193 ),
1194 )
1195 )
1196 elif token.endswith(_DEFAULT_TOKEN):
1197 raiseerr = False
1198
1199 for ent in entities:
1200 # return only the first _MapperEntity when searching
1201 # based on string prop name. Ideally object
1202 # attributes are used to specify more exactly.
1203 return ent
1204 else:
1205 if raiseerr:
1206 raise sa_exc.ArgumentError(
1207 "Query has only expression-based entities - "
1208 'can\'t find property named "%s".' % (token,)
1209 )
1210 else:
1211 return None
1212
1213
1214class loader_option(object):
1215 def __init__(self):
1216 pass
1217
1218 def __call__(self, fn):
1219 self.name = name = fn.__name__
1220 self.fn = fn
1221 if hasattr(Load, name):
1222 raise TypeError("Load class already has a %s method." % (name))
1223 setattr(Load, name, fn)
1224
1225 return self
1226
1227 def _add_unbound_fn(self, fn):
1228 self._unbound_fn = fn
1229 fn_doc = self.fn.__doc__
1230 self.fn.__doc__ = """Produce a new :class:`_orm.Load` object with the
1231:func:`_orm.%(name)s` option applied.
1232
1233See :func:`_orm.%(name)s` for usage examples.
1234
1235""" % {
1236 "name": self.name
1237 }
1238
1239 fn.__doc__ = fn_doc
1240 return self
1241
1242 def _add_unbound_all_fn(self, fn):
1243 fn.__doc__ = """Produce a standalone "all" option for
1244:func:`_orm.%(name)s`.
1245
1246.. deprecated:: 0.9
1247
1248 The :func:`_orm.%(name)s_all` function is deprecated, and will be removed
1249 in a future release. Please use method chaining with
1250 :func:`_orm.%(name)s` instead, as in::
1251
1252 session.query(MyClass).options(
1253 %(name)s("someattribute").%(name)s("anotherattribute")
1254 )
1255
1256""" % {
1257 "name": self.name
1258 }
1259 fn = util.deprecated(
1260 # This is used by `baked_lazyload_all` was only deprecated in
1261 # version 1.2 so this must stick around until that is removed
1262 "0.9",
1263 "The :func:`.%(name)s_all` function is deprecated, and will be "
1264 "removed in a future release. Please use method chaining with "
1265 ":func:`.%(name)s` instead" % {"name": self.name},
1266 add_deprecation_to_docstring=False,
1267 )(fn)
1268
1269 self._unbound_all_fn = fn
1270 return self
1271
1272
1273@loader_option()
1274def contains_eager(loadopt, attr, alias=None):
1275 r"""Indicate that the given attribute should be eagerly loaded from
1276 columns stated manually in the query.
1277
1278 This function is part of the :class:`_orm.Load` interface and supports
1279 both method-chained and standalone operation.
1280
1281 The option is used in conjunction with an explicit join that loads
1282 the desired rows, i.e.::
1283
1284 sess.query(Order).\
1285 join(Order.user).\
1286 options(contains_eager(Order.user))
1287
1288 The above query would join from the ``Order`` entity to its related
1289 ``User`` entity, and the returned ``Order`` objects would have the
1290 ``Order.user`` attribute pre-populated.
1291
1292 It may also be used for customizing the entries in an eagerly loaded
1293 collection; queries will normally want to use the
1294 :meth:`_query.Query.populate_existing` method assuming the primary
1295 collection of parent objects may already have been loaded::
1296
1297 sess.query(User).\
1298 join(User.addresses).\
1299 filter(Address.email_address.like('%@aol.com')).\
1300 options(contains_eager(User.addresses)).\
1301 populate_existing()
1302
1303 See the section :ref:`contains_eager` for complete usage details.
1304
1305 .. seealso::
1306
1307 :ref:`loading_toplevel`
1308
1309 :ref:`contains_eager`
1310
1311 """
1312 if alias is not None:
1313 if not isinstance(alias, str):
1314 info = inspect(alias)
1315 alias = info.selectable
1316
1317 else:
1318 util.warn_deprecated(
1319 "Passing a string name for the 'alias' argument to "
1320 "'contains_eager()` is deprecated, and will not work in a "
1321 "future release. Please use a sqlalchemy.alias() or "
1322 "sqlalchemy.orm.aliased() construct.",
1323 version="1.4",
1324 )
1325
1326 elif getattr(attr, "_of_type", None):
1327 ot = inspect(attr._of_type)
1328 alias = ot.selectable
1329
1330 cloned = loadopt.set_relationship_strategy(
1331 attr, {"lazy": "joined"}, propagate_to_loaders=False
1332 )
1333 cloned.local_opts["eager_from_alias"] = alias
1334 return cloned
1335
1336
1337@contains_eager._add_unbound_fn
1338def contains_eager(*keys, **kw):
1339 return _UnboundLoad()._from_keys(
1340 _UnboundLoad.contains_eager, keys, True, kw
1341 )
1342
1343
1344@loader_option()
1345def load_only(loadopt, *attrs):
1346 """Indicate that for a particular entity, only the given list
1347 of column-based attribute names should be loaded; all others will be
1348 deferred.
1349
1350 This function is part of the :class:`_orm.Load` interface and supports
1351 both method-chained and standalone operation.
1352
1353 Example - given a class ``User``, load only the ``name`` and ``fullname``
1354 attributes::
1355
1356 session.query(User).options(load_only(User.name, User.fullname))
1357
1358 Example - given a relationship ``User.addresses -> Address``, specify
1359 subquery loading for the ``User.addresses`` collection, but on each
1360 ``Address`` object load only the ``email_address`` attribute::
1361
1362 session.query(User).options(
1363 subqueryload(User.addresses).load_only(Address.email_address)
1364 )
1365
1366 For a :class:`_query.Query` that has multiple entities,
1367 the lead entity can be
1368 specifically referred to using the :class:`_orm.Load` constructor::
1369
1370 session.query(User, Address).join(User.addresses).options(
1371 Load(User).load_only(User.name, User.fullname),
1372 Load(Address).load_only(Address.email_address)
1373 )
1374
1375 .. note:: This method will still load a :class:`_schema.Column` even
1376 if the column property is defined with ``deferred=True``
1377 for the :func:`.column_property` function.
1378
1379 .. versionadded:: 0.9.0
1380
1381 """
1382 cloned = loadopt.set_column_strategy(
1383 attrs, {"deferred": False, "instrument": True}
1384 )
1385 cloned.set_column_strategy(
1386 "*", {"deferred": True, "instrument": True}, {"undefer_pks": True}
1387 )
1388 return cloned
1389
1390
1391@load_only._add_unbound_fn
1392def load_only(*attrs):
1393 return _UnboundLoad().load_only(*attrs)
1394
1395
1396@loader_option()
1397def joinedload(loadopt, attr, innerjoin=None):
1398 """Indicate that the given attribute should be loaded using joined
1399 eager loading.
1400
1401 This function is part of the :class:`_orm.Load` interface and supports
1402 both method-chained and standalone operation.
1403
1404 examples::
1405
1406 # joined-load the "orders" collection on "User"
1407 query(User).options(joinedload(User.orders))
1408
1409 # joined-load Order.items and then Item.keywords
1410 query(Order).options(
1411 joinedload(Order.items).joinedload(Item.keywords))
1412
1413 # lazily load Order.items, but when Items are loaded,
1414 # joined-load the keywords collection
1415 query(Order).options(
1416 lazyload(Order.items).joinedload(Item.keywords))
1417
1418 :param innerjoin: if ``True``, indicates that the joined eager load should
1419 use an inner join instead of the default of left outer join::
1420
1421 query(Order).options(joinedload(Order.user, innerjoin=True))
1422
1423 In order to chain multiple eager joins together where some may be
1424 OUTER and others INNER, right-nested joins are used to link them::
1425
1426 query(A).options(
1427 joinedload(A.bs, innerjoin=False).
1428 joinedload(B.cs, innerjoin=True)
1429 )
1430
1431 The above query, linking A.bs via "outer" join and B.cs via "inner" join
1432 would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When using
1433 older versions of SQLite (< 3.7.16), this form of JOIN is translated to
1434 use full subqueries as this syntax is otherwise not directly supported.
1435
1436 The ``innerjoin`` flag can also be stated with the term ``"unnested"``.
1437 This indicates that an INNER JOIN should be used, *unless* the join
1438 is linked to a LEFT OUTER JOIN to the left, in which case it
1439 will render as LEFT OUTER JOIN. For example, supposing ``A.bs``
1440 is an outerjoin::
1441
1442 query(A).options(
1443 joinedload(A.bs).
1444 joinedload(B.cs, innerjoin="unnested")
1445 )
1446
1447 The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c",
1448 rather than as "a LEFT OUTER JOIN (b JOIN c)".
1449
1450 .. note:: The "unnested" flag does **not** affect the JOIN rendered
1451 from a many-to-many association table, e.g. a table configured
1452 as :paramref:`_orm.relationship.secondary`, to the target table; for
1453 correctness of results, these joins are always INNER and are
1454 therefore right-nested if linked to an OUTER join.
1455
1456 .. versionchanged:: 1.0.0 ``innerjoin=True`` now implies
1457 ``innerjoin="nested"``, whereas in 0.9 it implied
1458 ``innerjoin="unnested"``. In order to achieve the pre-1.0 "unnested"
1459 inner join behavior, use the value ``innerjoin="unnested"``.
1460 See :ref:`migration_3008`.
1461
1462 .. note::
1463
1464 The joins produced by :func:`_orm.joinedload` are **anonymously
1465 aliased**. The criteria by which the join proceeds cannot be
1466 modified, nor can the :class:`_query.Query`
1467 refer to these joins in any way,
1468 including ordering. See :ref:`zen_of_eager_loading` for further
1469 detail.
1470
1471 To produce a specific SQL JOIN which is explicitly available, use
1472 :meth:`_query.Query.join`.
1473 To combine explicit JOINs with eager loading
1474 of collections, use :func:`_orm.contains_eager`; see
1475 :ref:`contains_eager`.
1476
1477 .. seealso::
1478
1479 :ref:`loading_toplevel`
1480
1481 :ref:`joined_eager_loading`
1482
1483 """
1484 loader = loadopt.set_relationship_strategy(attr, {"lazy": "joined"})
1485 if innerjoin is not None:
1486 loader.local_opts["innerjoin"] = innerjoin
1487 return loader
1488
1489
1490@joinedload._add_unbound_fn
1491def joinedload(*keys, **kw):
1492 return _UnboundLoad._from_keys(_UnboundLoad.joinedload, keys, False, kw)
1493
1494
1495@loader_option()
1496def subqueryload(loadopt, attr):
1497 """Indicate that the given attribute should be loaded using
1498 subquery eager loading.
1499
1500 This function is part of the :class:`_orm.Load` interface and supports
1501 both method-chained and standalone operation.
1502
1503 examples::
1504
1505 # subquery-load the "orders" collection on "User"
1506 query(User).options(subqueryload(User.orders))
1507
1508 # subquery-load Order.items and then Item.keywords
1509 query(Order).options(
1510 subqueryload(Order.items).subqueryload(Item.keywords))
1511
1512 # lazily load Order.items, but when Items are loaded,
1513 # subquery-load the keywords collection
1514 query(Order).options(
1515 lazyload(Order.items).subqueryload(Item.keywords))
1516
1517
1518 .. seealso::
1519
1520 :ref:`loading_toplevel`
1521
1522 :ref:`subquery_eager_loading`
1523
1524 """
1525 return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"})
1526
1527
1528@subqueryload._add_unbound_fn
1529def subqueryload(*keys):
1530 return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {})
1531
1532
1533@loader_option()
1534def selectinload(loadopt, attr):
1535 """Indicate that the given attribute should be loaded using
1536 SELECT IN eager loading.
1537
1538 This function is part of the :class:`_orm.Load` interface and supports
1539 both method-chained and standalone operation.
1540
1541 examples::
1542
1543 # selectin-load the "orders" collection on "User"
1544 query(User).options(selectinload(User.orders))
1545
1546 # selectin-load Order.items and then Item.keywords
1547 query(Order).options(
1548 selectinload(Order.items).selectinload(Item.keywords))
1549
1550 # lazily load Order.items, but when Items are loaded,
1551 # selectin-load the keywords collection
1552 query(Order).options(
1553 lazyload(Order.items).selectinload(Item.keywords))
1554
1555 .. versionadded:: 1.2
1556
1557 .. seealso::
1558
1559 :ref:`loading_toplevel`
1560
1561 :ref:`selectin_eager_loading`
1562
1563 """
1564 return loadopt.set_relationship_strategy(attr, {"lazy": "selectin"})
1565
1566
1567@selectinload._add_unbound_fn
1568def selectinload(*keys):
1569 return _UnboundLoad._from_keys(_UnboundLoad.selectinload, keys, False, {})
1570
1571
1572@loader_option()
1573def lazyload(loadopt, attr):
1574 """Indicate that the given attribute should be loaded using "lazy"
1575 loading.
1576
1577 This function is part of the :class:`_orm.Load` interface and supports
1578 both method-chained and standalone operation.
1579
1580 .. seealso::
1581
1582 :ref:`loading_toplevel`
1583
1584 :ref:`lazy_loading`
1585
1586 """
1587 return loadopt.set_relationship_strategy(attr, {"lazy": "select"})
1588
1589
1590@lazyload._add_unbound_fn
1591def lazyload(*keys):
1592 return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {})
1593
1594
1595@loader_option()
1596def immediateload(loadopt, attr):
1597 """Indicate that the given attribute should be loaded using
1598 an immediate load with a per-attribute SELECT statement.
1599
1600 The load is achieved using the "lazyloader" strategy and does not
1601 fire off any additional eager loaders.
1602
1603 The :func:`.immediateload` option is superseded in general
1604 by the :func:`.selectinload` option, which performs the same task
1605 more efficiently by emitting a SELECT for all loaded objects.
1606
1607 This function is part of the :class:`_orm.Load` interface and supports
1608 both method-chained and standalone operation.
1609
1610 .. seealso::
1611
1612 :ref:`loading_toplevel`
1613
1614 :ref:`selectin_eager_loading`
1615
1616 """
1617 loader = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"})
1618 return loader
1619
1620
1621@immediateload._add_unbound_fn
1622def immediateload(*keys):
1623 return _UnboundLoad._from_keys(_UnboundLoad.immediateload, keys, False, {})
1624
1625
1626@loader_option()
1627def noload(loadopt, attr):
1628 """Indicate that the given relationship attribute should remain unloaded.
1629
1630 The relationship attribute will return ``None`` when accessed without
1631 producing any loading effect.
1632
1633 This function is part of the :class:`_orm.Load` interface and supports
1634 both method-chained and standalone operation.
1635
1636 :func:`_orm.noload` applies to :func:`_orm.relationship` attributes; for
1637 column-based attributes, see :func:`_orm.defer`.
1638
1639 .. note:: Setting this loading strategy as the default strategy
1640 for a relationship using the :paramref:`.orm.relationship.lazy`
1641 parameter may cause issues with flushes, such if a delete operation
1642 needs to load related objects and instead ``None`` was returned.
1643
1644 .. seealso::
1645
1646 :ref:`loading_toplevel`
1647
1648 """
1649
1650 return loadopt.set_relationship_strategy(attr, {"lazy": "noload"})
1651
1652
1653@noload._add_unbound_fn
1654def noload(*keys):
1655 return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {})
1656
1657
1658@loader_option()
1659def raiseload(loadopt, attr, sql_only=False):
1660 """Indicate that the given attribute should raise an error if accessed.
1661
1662 A relationship attribute configured with :func:`_orm.raiseload` will
1663 raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The
1664 typical way this is useful is when an application is attempting to ensure
1665 that all relationship attributes that are accessed in a particular context
1666 would have been already loaded via eager loading. Instead of having
1667 to read through SQL logs to ensure lazy loads aren't occurring, this
1668 strategy will cause them to raise immediately.
1669
1670 :func:`_orm.raiseload` applies to :func:`_orm.relationship`
1671 attributes only.
1672 In order to apply raise-on-SQL behavior to a column-based attribute,
1673 use the :paramref:`.orm.defer.raiseload` parameter on the :func:`.defer`
1674 loader option.
1675
1676 :param sql_only: if True, raise only if the lazy load would emit SQL, but
1677 not if it is only checking the identity map, or determining that the
1678 related value should just be None due to missing keys. When False, the
1679 strategy will raise for all varieties of relationship loading.
1680
1681 This function is part of the :class:`_orm.Load` interface and supports
1682 both method-chained and standalone operation.
1683
1684
1685 .. versionadded:: 1.1
1686
1687 .. seealso::
1688
1689 :ref:`loading_toplevel`
1690
1691 :ref:`prevent_lazy_with_raiseload`
1692
1693 :ref:`deferred_raiseload`
1694
1695 """
1696
1697 return loadopt.set_relationship_strategy(
1698 attr, {"lazy": "raise_on_sql" if sql_only else "raise"}
1699 )
1700
1701
1702@raiseload._add_unbound_fn
1703def raiseload(*keys, **kw):
1704 return _UnboundLoad._from_keys(_UnboundLoad.raiseload, keys, False, kw)
1705
1706
1707@loader_option()
1708def defaultload(loadopt, attr):
1709 """Indicate an attribute should load using its default loader style.
1710
1711 This method is used to link to other loader options further into
1712 a chain of attributes without altering the loader style of the links
1713 along the chain. For example, to set joined eager loading for an
1714 element of an element::
1715
1716 session.query(MyClass).options(
1717 defaultload(MyClass.someattribute).
1718 joinedload(MyOtherClass.someotherattribute)
1719 )
1720
1721 :func:`.defaultload` is also useful for setting column-level options
1722 on a related class, namely that of :func:`.defer` and :func:`.undefer`::
1723
1724 session.query(MyClass).options(
1725 defaultload(MyClass.someattribute).
1726 defer("some_column").
1727 undefer("some_other_column")
1728 )
1729
1730 .. seealso::
1731
1732 :meth:`_orm.Load.options` - allows for complex hierarchical
1733 loader option structures with less verbosity than with individual
1734 :func:`.defaultload` directives.
1735
1736 :ref:`relationship_loader_options`
1737
1738 :ref:`deferred_loading_w_multiple`
1739
1740 """
1741 return loadopt.set_relationship_strategy(attr, None)
1742
1743
1744@defaultload._add_unbound_fn
1745def defaultload(*keys):
1746 return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {})
1747
1748
1749@loader_option()
1750def defer(loadopt, key, raiseload=False):
1751 r"""Indicate that the given column-oriented attribute should be deferred,
1752 e.g. not loaded until accessed.
1753
1754 This function is part of the :class:`_orm.Load` interface and supports
1755 both method-chained and standalone operation.
1756
1757 e.g.::
1758
1759 from sqlalchemy.orm import defer
1760
1761 session.query(MyClass).options(
1762 defer("attribute_one"),
1763 defer("attribute_two"))
1764
1765 session.query(MyClass).options(
1766 defer(MyClass.attribute_one),
1767 defer(MyClass.attribute_two))
1768
1769 To specify a deferred load of an attribute on a related class,
1770 the path can be specified one token at a time, specifying the loading
1771 style for each link along the chain. To leave the loading style
1772 for a link unchanged, use :func:`_orm.defaultload`::
1773
1774 session.query(MyClass).options(defaultload("someattr").defer("some_column"))
1775
1776 A :class:`_orm.Load` object that is present on a certain path can have
1777 :meth:`_orm.Load.defer` called multiple times,
1778 each will operate on the same
1779 parent entity::
1780
1781
1782 session.query(MyClass).options(
1783 defaultload("someattr").
1784 defer("some_column").
1785 defer("some_other_column").
1786 defer("another_column")
1787 )
1788
1789 :param key: Attribute to be deferred.
1790
1791 :param raiseload: raise :class:`.InvalidRequestError` if the column
1792 value is to be loaded from emitting SQL. Used to prevent unwanted
1793 SQL from being emitted.
1794
1795 .. versionadded:: 1.4
1796
1797 .. seealso::
1798
1799 :ref:`deferred_raiseload`
1800
1801 :param \*addl_attrs: This option supports the old 0.8 style
1802 of specifying a path as a series of attributes, which is now superseded
1803 by the method-chained style.
1804
1805 .. deprecated:: 0.9 The \*addl_attrs on :func:`_orm.defer` is
1806 deprecated and will be removed in a future release. Please
1807 use method chaining in conjunction with defaultload() to
1808 indicate a path.
1809
1810
1811 .. seealso::
1812
1813 :ref:`deferred`
1814
1815 :func:`_orm.undefer`
1816
1817 """
1818 strategy = {"deferred": True, "instrument": True}
1819 if raiseload:
1820 strategy["raiseload"] = True
1821 return loadopt.set_column_strategy((key,), strategy)
1822
1823
1824@defer._add_unbound_fn
1825def defer(key, *addl_attrs, **kw):
1826 if addl_attrs:
1827 util.warn_deprecated(
1828 "The *addl_attrs on orm.defer is deprecated. Please use "
1829 "method chaining in conjunction with defaultload() to "
1830 "indicate a path.",
1831 version="1.3",
1832 )
1833 return _UnboundLoad._from_keys(
1834 _UnboundLoad.defer, (key,) + addl_attrs, False, kw
1835 )
1836
1837
1838@loader_option()
1839def undefer(loadopt, key):
1840 r"""Indicate that the given column-oriented attribute should be undeferred,
1841 e.g. specified within the SELECT statement of the entity as a whole.
1842
1843 The column being undeferred is typically set up on the mapping as a
1844 :func:`.deferred` attribute.
1845
1846 This function is part of the :class:`_orm.Load` interface and supports
1847 both method-chained and standalone operation.
1848
1849 Examples::
1850
1851 # undefer two columns
1852 session.query(MyClass).options(undefer("col1"), undefer("col2"))
1853
1854 # undefer all columns specific to a single class using Load + *
1855 session.query(MyClass, MyOtherClass).options(
1856 Load(MyClass).undefer("*"))
1857
1858 # undefer a column on a related object
1859 session.query(MyClass).options(
1860 defaultload(MyClass.items).undefer('text'))
1861
1862 :param key: Attribute to be undeferred.
1863
1864 :param \*addl_attrs: This option supports the old 0.8 style
1865 of specifying a path as a series of attributes, which is now superseded
1866 by the method-chained style.
1867
1868 .. deprecated:: 0.9 The \*addl_attrs on :func:`_orm.undefer` is
1869 deprecated and will be removed in a future release. Please
1870 use method chaining in conjunction with defaultload() to
1871 indicate a path.
1872
1873 .. seealso::
1874
1875 :ref:`deferred`
1876
1877 :func:`_orm.defer`
1878
1879 :func:`_orm.undefer_group`
1880
1881 """
1882 return loadopt.set_column_strategy(
1883 (key,), {"deferred": False, "instrument": True}
1884 )
1885
1886
1887@undefer._add_unbound_fn
1888def undefer(key, *addl_attrs):
1889 if addl_attrs:
1890 util.warn_deprecated(
1891 "The *addl_attrs on orm.undefer is deprecated. Please use "
1892 "method chaining in conjunction with defaultload() to "
1893 "indicate a path.",
1894 version="1.3",
1895 )
1896 return _UnboundLoad._from_keys(
1897 _UnboundLoad.undefer, (key,) + addl_attrs, False, {}
1898 )
1899
1900
1901@loader_option()
1902def undefer_group(loadopt, name):
1903 """Indicate that columns within the given deferred group name should be
1904 undeferred.
1905
1906 The columns being undeferred are set up on the mapping as
1907 :func:`.deferred` attributes and include a "group" name.
1908
1909 E.g::
1910
1911 session.query(MyClass).options(undefer_group("large_attrs"))
1912
1913 To undefer a group of attributes on a related entity, the path can be
1914 spelled out using relationship loader options, such as
1915 :func:`_orm.defaultload`::
1916
1917 session.query(MyClass).options(
1918 defaultload("someattr").undefer_group("large_attrs"))
1919
1920 .. versionchanged:: 0.9.0 :func:`_orm.undefer_group` is now specific to a
1921 particular entity load path.
1922
1923 .. seealso::
1924
1925 :ref:`deferred`
1926
1927 :func:`_orm.defer`
1928
1929 :func:`_orm.undefer`
1930
1931 """
1932 return loadopt.set_column_strategy(
1933 "*", None, {"undefer_group_%s" % name: True}, opts_only=True
1934 )
1935
1936
1937@undefer_group._add_unbound_fn
1938def undefer_group(name):
1939 return _UnboundLoad().undefer_group(name)
1940
1941
1942@loader_option()
1943def with_expression(loadopt, key, expression):
1944 r"""Apply an ad-hoc SQL expression to a "deferred expression" attribute.
1945
1946 This option is used in conjunction with the :func:`_orm.query_expression`
1947 mapper-level construct that indicates an attribute which should be the
1948 target of an ad-hoc SQL expression.
1949
1950 E.g.::
1951
1952
1953 sess.query(SomeClass).options(
1954 with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y)
1955 )
1956
1957 .. versionadded:: 1.2
1958
1959 :param key: Attribute to be populated.
1960
1961 :param expr: SQL expression to be applied to the attribute.
1962
1963 .. versionchanged:: 1.4 Loader options such as
1964 :func:`_orm.with_expression`
1965 take effect only at the **outermost** query used, and should not be used
1966 within subqueries or inner elements of a UNION. See the change notes at
1967 :ref:`change_8879` for background on how to correctly add arbitrary
1968 columns to subqueries.
1969
1970 .. note:: the target attribute is populated only if the target object
1971 is **not currently loaded** in the current :class:`_orm.Session`
1972 unless the :meth:`_query.Query.populate_existing` method is used.
1973 Please refer to :ref:`mapper_querytime_expression` for complete
1974 usage details.
1975
1976 .. seealso::
1977
1978 :ref:`mapper_querytime_expression`
1979
1980 """
1981
1982 expression = coercions.expect(
1983 roles.LabeledColumnExprRole, _orm_full_deannotate(expression)
1984 )
1985
1986 return loadopt.set_column_strategy(
1987 (key,), {"query_expression": True}, opts={"expression": expression}
1988 )
1989
1990
1991@with_expression._add_unbound_fn
1992def with_expression(key, expression):
1993 return _UnboundLoad._from_keys(
1994 _UnboundLoad.with_expression, (key,), False, {"expression": expression}
1995 )
1996
1997
1998@loader_option()
1999def selectin_polymorphic(loadopt, classes):
2000 """Indicate an eager load should take place for all attributes
2001 specific to a subclass.
2002
2003 This uses an additional SELECT with IN against all matched primary
2004 key values, and is the per-query analogue to the ``"selectin"``
2005 setting on the :paramref:`.mapper.polymorphic_load` parameter.
2006
2007 .. versionadded:: 1.2
2008
2009 .. seealso::
2010
2011 :ref:`polymorphic_selectin`
2012
2013 """
2014 loadopt.set_class_strategy(
2015 {"selectinload_polymorphic": True},
2016 opts={
2017 "entities": tuple(
2018 sorted((inspect(cls) for cls in classes), key=id)
2019 )
2020 },
2021 )
2022 return loadopt
2023
2024
2025@selectin_polymorphic._add_unbound_fn
2026def selectin_polymorphic(base_cls, classes):
2027 ul = _UnboundLoad()
2028 ul.is_class_strategy = True
2029 ul.path = (inspect(base_cls),)
2030 ul.selectin_polymorphic(classes)
2031 return ul