summaryrefslogtreecommitdiffstats
path: root/lib/sqlalchemy/orm/strategy_options.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm/strategy_options.py')
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py2008
1 files changed, 2008 insertions, 0 deletions
diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py
new file mode 100644
index 0000000..c3dd5df
--- /dev/null
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -0,0 +1,2008 @@
+# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+"""
+
+"""
+
+from . import util as orm_util
+from .attributes import QueryableAttribute
+from .base import _class_to_mapper
+from .base import _is_aliased_class
+from .base import _is_mapped_class
+from .base import InspectionAttr
+from .interfaces import LoaderOption
+from .interfaces import MapperProperty
+from .interfaces import PropComparator
+from .path_registry import _DEFAULT_TOKEN
+from .path_registry import _WILDCARD_TOKEN
+from .path_registry import PathRegistry
+from .path_registry import TokenRegistry
+from .util import _orm_full_deannotate
+from .. import exc as sa_exc
+from .. import inspect
+from .. import util
+from ..sql import and_
+from ..sql import coercions
+from ..sql import roles
+from ..sql import traversals
+from ..sql import visitors
+from ..sql.base import _generative
+from ..sql.base import Generative
+
+
+class Load(Generative, LoaderOption):
+ """Represents loader options which modify the state of a
+ :class:`_query.Query` in order to affect how various mapped attributes are
+ loaded.
+
+ The :class:`_orm.Load` object is in most cases used implicitly behind the
+ scenes when one makes use of a query option like :func:`_orm.joinedload`,
+ :func:`.defer`, or similar. However, the :class:`_orm.Load` object
+ can also be used directly, and in some cases can be useful.
+
+ To use :class:`_orm.Load` directly, instantiate it with the target mapped
+ class as the argument. This style of usage is
+ useful when dealing with a :class:`_query.Query`
+ that has multiple entities::
+
+ myopt = Load(MyClass).joinedload("widgets")
+
+ The above ``myopt`` can now be used with :meth:`_query.Query.options`,
+ where it
+ will only take effect for the ``MyClass`` entity::
+
+ session.query(MyClass, MyOtherClass).options(myopt)
+
+ One case where :class:`_orm.Load`
+ is useful as public API is when specifying
+ "wildcard" options that only take effect for a certain class::
+
+ session.query(Order).options(Load(Order).lazyload('*'))
+
+ Above, all relationships on ``Order`` will be lazy-loaded, but other
+ attributes on those descendant objects will load using their normal
+ loader strategy.
+
+ .. seealso::
+
+ :ref:`deferred_options`
+
+ :ref:`deferred_loading_w_multiple`
+
+ :ref:`relationship_loader_options`
+
+ """
+
+ _is_strategy_option = True
+
+ _cache_key_traversal = [
+ ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key),
+ ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
+ ("_of_type", visitors.ExtendedInternalTraversal.dp_multi),
+ ("_extra_criteria", visitors.InternalTraversal.dp_clauseelement_list),
+ (
+ "_context_cache_key",
+ visitors.ExtendedInternalTraversal.dp_has_cache_key_tuples,
+ ),
+ (
+ "local_opts",
+ visitors.ExtendedInternalTraversal.dp_string_multi_dict,
+ ),
+ ]
+
+ def __init__(self, entity):
+ insp = inspect(entity)
+ insp._post_inspect
+
+ self.path = insp._path_registry
+ # note that this .context is shared among all descendant
+ # Load objects
+ self.context = util.OrderedDict()
+ self.local_opts = {}
+ self.is_class_strategy = False
+
+ @classmethod
+ def for_existing_path(cls, path):
+ load = cls.__new__(cls)
+ load.path = path
+ load.context = {}
+ load.local_opts = {}
+ load._of_type = None
+ load._extra_criteria = ()
+ return load
+
+ def _generate_extra_criteria(self, context):
+ """Apply the current bound parameters in a QueryContext to the
+ immediate "extra_criteria" stored with this Load object.
+
+ Load objects are typically pulled from the cached version of
+ the statement from a QueryContext. The statement currently being
+ executed will have new values (and keys) for bound parameters in the
+ extra criteria which need to be applied by loader strategies when
+ they handle this criteria for a result set.
+
+ """
+
+ assert (
+ self._extra_criteria
+ ), "this should only be called if _extra_criteria is present"
+
+ orig_query = context.compile_state.select_statement
+ current_query = context.query
+
+ # NOTE: while it seems like we should not do the "apply" operation
+ # here if orig_query is current_query, skipping it in the "optimized"
+ # case causes the query to be different from a cache key perspective,
+ # because we are creating a copy of the criteria which is no longer
+ # the same identity of the _extra_criteria in the loader option
+ # itself. cache key logic produces a different key for
+ # (A, copy_of_A) vs. (A, A), because in the latter case it shortens
+ # the second part of the key to just indicate on identity.
+
+ # if orig_query is current_query:
+ # not cached yet. just do the and_()
+ # return and_(*self._extra_criteria)
+
+ k1 = orig_query._generate_cache_key()
+ k2 = current_query._generate_cache_key()
+
+ return k2._apply_params_to_element(k1, and_(*self._extra_criteria))
+
+ def _adjust_for_extra_criteria(self, context):
+ """Apply the current bound parameters in a QueryContext to all
+ occurrences "extra_criteria" stored within al this Load object;
+ copying in place.
+
+ """
+ orig_query = context.compile_state.select_statement
+
+ applied = {}
+
+ ck = [None, None]
+
+ def process(opt):
+ if not opt._extra_criteria:
+ return
+
+ if ck[0] is None:
+ ck[:] = (
+ orig_query._generate_cache_key(),
+ context.query._generate_cache_key(),
+ )
+ k1, k2 = ck
+
+ opt._extra_criteria = tuple(
+ k2._apply_params_to_element(k1, crit)
+ for crit in opt._extra_criteria
+ )
+
+ return self._deep_clone(applied, process)
+
+ def _deep_clone(self, applied, process):
+ if self in applied:
+ return applied[self]
+
+ cloned = self._generate()
+
+ applied[self] = cloned
+
+ cloned.strategy = self.strategy
+
+ assert cloned.propagate_to_loaders == self.propagate_to_loaders
+ assert cloned.is_class_strategy == self.is_class_strategy
+ assert cloned.is_opts_only == self.is_opts_only
+
+ if self.context:
+ cloned.context = util.OrderedDict(
+ [
+ (
+ key,
+ value._deep_clone(applied, process)
+ if isinstance(value, Load)
+ else value,
+ )
+ for key, value in self.context.items()
+ ]
+ )
+
+ cloned.local_opts.update(self.local_opts)
+
+ process(cloned)
+
+ return cloned
+
+ @property
+ def _context_cache_key(self):
+ serialized = []
+ if self.context is None:
+ return []
+ for (key, loader_path), obj in self.context.items():
+ if key != "loader":
+ continue
+ serialized.append(loader_path + (obj,))
+ return serialized
+
+ def _generate(self):
+ cloned = super(Load, self)._generate()
+ cloned.local_opts = {}
+ return cloned
+
+ is_opts_only = False
+ is_class_strategy = False
+ strategy = None
+ propagate_to_loaders = False
+ _of_type = None
+ _extra_criteria = ()
+
+ def process_compile_state_replaced_entities(
+ self, compile_state, mapper_entities
+ ):
+ if not compile_state.compile_options._enable_eagerloads:
+ return
+
+ # process is being run here so that the options given are validated
+ # against what the lead entities were, as well as to accommodate
+ # for the entities having been replaced with equivalents
+ self._process(
+ compile_state,
+ mapper_entities,
+ not bool(compile_state.current_path),
+ )
+
+ def process_compile_state(self, compile_state):
+ if not compile_state.compile_options._enable_eagerloads:
+ return
+
+ self._process(
+ compile_state,
+ compile_state._lead_mapper_entities,
+ not bool(compile_state.current_path)
+ and not compile_state.compile_options._for_refresh_state,
+ )
+
+ def _process(self, compile_state, mapper_entities, raiseerr):
+ is_refresh = compile_state.compile_options._for_refresh_state
+ current_path = compile_state.current_path
+ if current_path:
+ for (token, start_path), loader in self.context.items():
+ if is_refresh and not loader.propagate_to_loaders:
+ continue
+ chopped_start_path = self._chop_path(start_path, current_path)
+ if chopped_start_path is not None:
+ compile_state.attributes[
+ (token, chopped_start_path)
+ ] = loader
+ else:
+ compile_state.attributes.update(self.context)
+
+ def _generate_path(
+ self,
+ path,
+ attr,
+ for_strategy,
+ wildcard_key,
+ raiseerr=True,
+ polymorphic_entity_context=None,
+ ):
+ existing_of_type = self._of_type
+ self._of_type = None
+ if raiseerr and not path.has_entity:
+ if isinstance(path, TokenRegistry):
+ raise sa_exc.ArgumentError(
+ "Wildcard token cannot be followed by another entity"
+ )
+ else:
+ raise sa_exc.ArgumentError(
+ "Mapped attribute '%s' does not "
+ "refer to a mapped entity" % (path.prop,)
+ )
+
+ if isinstance(attr, util.string_types):
+
+ default_token = attr.endswith(_DEFAULT_TOKEN)
+ attr_str_name = attr
+ if attr.endswith(_WILDCARD_TOKEN) or default_token:
+ if default_token:
+ self.propagate_to_loaders = False
+ if wildcard_key:
+ attr = "%s:%s" % (wildcard_key, attr)
+
+ # TODO: AliasedInsp inside the path for of_type is not
+ # working for a with_polymorphic entity because the
+ # relationship loaders don't render the with_poly into the
+ # path. See #4469 which will try to improve this
+ if existing_of_type and not existing_of_type.is_aliased_class:
+ path = path.parent[existing_of_type]
+ path = path.token(attr)
+ self.path = path
+ return path
+
+ if existing_of_type:
+ ent = inspect(existing_of_type)
+ else:
+ ent = path.entity
+
+ util.warn_deprecated_20(
+ "Using strings to indicate column or "
+ "relationship paths in loader options is deprecated "
+ "and will be removed in SQLAlchemy 2.0. Please use "
+ "the class-bound attribute directly.",
+ )
+ try:
+ # use getattr on the class to work around
+ # synonyms, hybrids, etc.
+ attr = getattr(ent.class_, attr)
+ except AttributeError as err:
+ if raiseerr:
+ util.raise_(
+ sa_exc.ArgumentError(
+ 'Can\'t find property named "%s" on '
+ "%s in this Query." % (attr, ent)
+ ),
+ replace_context=err,
+ )
+ else:
+ return None
+ else:
+ try:
+ attr = found_property = attr.property
+ except AttributeError as ae:
+ if not isinstance(attr, MapperProperty):
+ util.raise_(
+ sa_exc.ArgumentError(
+ 'Expected attribute "%s" on %s to be a '
+ "mapped attribute; "
+ "instead got %s object."
+ % (attr_str_name, ent, type(attr))
+ ),
+ replace_context=ae,
+ )
+ else:
+ raise
+
+ path = path[attr]
+ else:
+ insp = inspect(attr)
+
+ if insp.is_mapper or insp.is_aliased_class:
+ # TODO: this does not appear to be a valid codepath. "attr"
+ # would never be a mapper. This block is present in 1.2
+ # as well however does not seem to be accessed in any tests.
+ if not orm_util._entity_corresponds_to_use_path_impl(
+ attr.parent, path[-1]
+ ):
+ if raiseerr:
+ raise sa_exc.ArgumentError(
+ "Attribute '%s' does not "
+ "link from element '%s'" % (attr, path.entity)
+ )
+ else:
+ return None
+ elif insp.is_property:
+ prop = found_property = attr
+ path = path[prop]
+ elif insp.is_attribute:
+ prop = found_property = attr.property
+
+ if not orm_util._entity_corresponds_to_use_path_impl(
+ attr.parent, path[-1]
+ ):
+ if raiseerr:
+ raise sa_exc.ArgumentError(
+ 'Attribute "%s" does not '
+ 'link from element "%s".%s'
+ % (
+ attr,
+ path.entity,
+ (
+ " Did you mean to use "
+ "%s.of_type(%s)?"
+ % (path[-2], attr.class_.__name__)
+ if len(path) > 1
+ and path.entity.is_mapper
+ and attr.parent.is_aliased_class
+ else ""
+ ),
+ )
+ )
+ else:
+ return None
+
+ if attr._extra_criteria and not self._extra_criteria:
+ # in most cases, the process that brings us here will have
+ # already established _extra_criteria. however if not,
+ # and it's present on the attribute, then use that.
+ self._extra_criteria = attr._extra_criteria
+
+ if getattr(attr, "_of_type", None):
+ ac = attr._of_type
+ ext_info = of_type_info = inspect(ac)
+
+ if polymorphic_entity_context is None:
+ polymorphic_entity_context = self.context
+
+ existing = path.entity_path[prop].get(
+ polymorphic_entity_context, "path_with_polymorphic"
+ )
+
+ if not ext_info.is_aliased_class:
+ ac = orm_util.with_polymorphic(
+ ext_info.mapper.base_mapper,
+ ext_info.mapper,
+ aliased=True,
+ _use_mapper_path=True,
+ _existing_alias=inspect(existing)
+ if existing is not None
+ else None,
+ )
+
+ ext_info = inspect(ac)
+
+ path.entity_path[prop].set(
+ polymorphic_entity_context, "path_with_polymorphic", ac
+ )
+
+ path = path[prop][ext_info]
+
+ self._of_type = of_type_info
+
+ else:
+ path = path[prop]
+
+ if for_strategy is not None:
+ found_property._get_strategy(for_strategy)
+ if path.has_entity:
+ path = path.entity_path
+ self.path = path
+ return path
+
+ def __str__(self):
+ return "Load(strategy=%r)" % (self.strategy,)
+
+ def _coerce_strat(self, strategy):
+ if strategy is not None:
+ strategy = tuple(sorted(strategy.items()))
+ return strategy
+
+ def _apply_to_parent(self, parent, applied, bound):
+ raise NotImplementedError(
+ "Only 'unbound' loader options may be used with the "
+ "Load.options() method"
+ )
+
+ @_generative
+ def options(self, *opts):
+ r"""Apply a series of options as sub-options to this
+ :class:`_orm.Load`
+ object.
+
+ E.g.::
+
+ query = session.query(Author)
+ query = query.options(
+ joinedload(Author.book).options(
+ load_only(Book.summary, Book.excerpt),
+ joinedload(Book.citations).options(
+ joinedload(Citation.author)
+ )
+ )
+ )
+
+ :param \*opts: A series of loader option objects (ultimately
+ :class:`_orm.Load` objects) which should be applied to the path
+ specified by this :class:`_orm.Load` object.
+
+ .. versionadded:: 1.3.6
+
+ .. seealso::
+
+ :func:`.defaultload`
+
+ :ref:`relationship_loader_options`
+
+ :ref:`deferred_loading_w_multiple`
+
+ """
+ apply_cache = {}
+ bound = not isinstance(self, _UnboundLoad)
+ if bound:
+ raise NotImplementedError(
+ "The options() method is currently only supported "
+ "for 'unbound' loader options"
+ )
+ for opt in opts:
+ opt._apply_to_parent(self, apply_cache, bound)
+
+ @_generative
+ def set_relationship_strategy(
+ self, attr, strategy, propagate_to_loaders=True
+ ):
+ strategy = self._coerce_strat(strategy)
+ self.propagate_to_loaders = propagate_to_loaders
+ cloned = self._clone_for_bind_strategy(attr, strategy, "relationship")
+ self.path = cloned.path
+ self._of_type = cloned._of_type
+ self._extra_criteria = cloned._extra_criteria
+ cloned.is_class_strategy = self.is_class_strategy = False
+ self.propagate_to_loaders = cloned.propagate_to_loaders
+
+ @_generative
+ def set_column_strategy(self, attrs, strategy, opts=None, opts_only=False):
+ strategy = self._coerce_strat(strategy)
+ self.is_class_strategy = False
+ for attr in attrs:
+ cloned = self._clone_for_bind_strategy(
+ attr, strategy, "column", opts_only=opts_only, opts=opts
+ )
+ cloned.propagate_to_loaders = True
+
+ @_generative
+ def set_generic_strategy(self, attrs, strategy):
+ strategy = self._coerce_strat(strategy)
+ for attr in attrs:
+ cloned = self._clone_for_bind_strategy(attr, strategy, None)
+ cloned.propagate_to_loaders = True
+
+ @_generative
+ def set_class_strategy(self, strategy, opts):
+ strategy = self._coerce_strat(strategy)
+ cloned = self._clone_for_bind_strategy(None, strategy, None)
+ cloned.is_class_strategy = True
+ cloned.propagate_to_loaders = True
+ cloned.local_opts.update(opts)
+
+ def _clone_for_bind_strategy(
+ self, attr, strategy, wildcard_key, opts_only=False, opts=None
+ ):
+ """Create an anonymous clone of the Load/_UnboundLoad that is suitable
+ to be placed in the context / _to_bind collection of this Load
+ object. The clone will then lose references to context/_to_bind
+ in order to not create reference cycles.
+
+ """
+ cloned = self._generate()
+ cloned._generate_path(self.path, attr, strategy, wildcard_key)
+ cloned.strategy = strategy
+
+ cloned.local_opts = self.local_opts
+ if opts:
+ cloned.local_opts.update(opts)
+ if opts_only:
+ cloned.is_opts_only = True
+
+ if strategy or cloned.is_opts_only:
+ cloned._set_path_strategy()
+ return cloned
+
+ def _set_for_path(self, context, path, replace=True, merge_opts=False):
+ if merge_opts or not replace:
+ existing = path.get(context, "loader")
+ if existing:
+ if merge_opts:
+ existing.local_opts.update(self.local_opts)
+ existing._extra_criteria += self._extra_criteria
+ else:
+ path.set(context, "loader", self)
+ else:
+ existing = path.get(context, "loader")
+ path.set(context, "loader", self)
+ if existing and existing.is_opts_only:
+ self.local_opts.update(existing.local_opts)
+ existing._extra_criteria += self._extra_criteria
+
+ def _set_path_strategy(self):
+ if not self.is_class_strategy and self.path.has_entity:
+ effective_path = self.path.parent
+ else:
+ effective_path = self.path
+
+ if effective_path.is_token:
+ for path in effective_path.generate_for_superclasses():
+ self._set_for_path(
+ self.context,
+ path,
+ replace=True,
+ merge_opts=self.is_opts_only,
+ )
+ else:
+ self._set_for_path(
+ self.context,
+ effective_path,
+ replace=True,
+ merge_opts=self.is_opts_only,
+ )
+
+ # remove cycles; _set_path_strategy is always invoked on an
+ # anonymous clone of the Load / UnboundLoad object since #5056
+ self.context = None
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+
+ # can't pickle this right now; warning is raised by strategies
+ d["_extra_criteria"] = ()
+
+ if d["context"] is not None:
+ d["context"] = PathRegistry.serialize_context_dict(
+ d["context"], ("loader",)
+ )
+ d["path"] = self.path.serialize()
+ return d
+
+ def __setstate__(self, state):
+ self.__dict__.update(state)
+ self.path = PathRegistry.deserialize(self.path)
+ if self.context is not None:
+ self.context = PathRegistry.deserialize_context_dict(self.context)
+
+ def _chop_path(self, to_chop, path):
+ i = -1
+
+ for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)):
+ if isinstance(c_token, util.string_types):
+ # TODO: this is approximated from the _UnboundLoad
+ # version and probably has issues, not fully covered.
+
+ if i == 0 and c_token.endswith(":" + _DEFAULT_TOKEN):
+ return to_chop
+ elif (
+ c_token != "relationship:%s" % (_WILDCARD_TOKEN,)
+ and c_token != p_token.key
+ ):
+ return None
+
+ if c_token is p_token:
+ continue
+ elif (
+ isinstance(c_token, InspectionAttr)
+ and c_token.is_mapper
+ and p_token.is_mapper
+ and c_token.isa(p_token)
+ ):
+ continue
+ else:
+ return None
+ return to_chop[i + 1 :]
+
+
+class _UnboundLoad(Load):
+ """Represent a loader option that isn't tied to a root entity.
+
+ The loader option will produce an entity-linked :class:`_orm.Load`
+ object when it is passed :meth:`_query.Query.options`.
+
+ This provides compatibility with the traditional system
+ of freestanding options, e.g. ``joinedload('x.y.z')``.
+
+ """
+
+ def __init__(self):
+ self.path = ()
+ self._to_bind = []
+ self.local_opts = {}
+ self._extra_criteria = ()
+
+ def _gen_cache_key(self, anon_map, bindparams, _unbound_option_seen=None):
+ """Inlined gen_cache_key
+
+ Original traversal is::
+
+
+ _cache_key_traversal = [
+ ("path", visitors.ExtendedInternalTraversal.dp_multi_list),
+ ("strategy", visitors.ExtendedInternalTraversal.dp_plain_obj),
+ (
+ "_to_bind",
+ visitors.ExtendedInternalTraversal.dp_has_cache_key_list,
+ ),
+ (
+ "_extra_criteria",
+ visitors.InternalTraversal.dp_clauseelement_list),
+ (
+ "local_opts",
+ visitors.ExtendedInternalTraversal.dp_string_multi_dict,
+ ),
+ ]
+
+ The inlining is so that the "_to_bind" list can be flattened to not
+ repeat the same UnboundLoad options over and over again.
+
+ See #6869
+
+ """
+
+ idself = id(self)
+ cls = self.__class__
+
+ if idself in anon_map:
+ return (anon_map[idself], cls)
+ else:
+ id_ = anon_map[idself]
+
+ vis = traversals._cache_key_traversal_visitor
+
+ seen = _unbound_option_seen
+ if seen is None:
+ seen = set()
+
+ return (
+ (id_, cls)
+ + vis.visit_multi_list(
+ "path", self.path, self, anon_map, bindparams
+ )
+ + ("strategy", self.strategy)
+ + (
+ (
+ "_to_bind",
+ tuple(
+ elem._gen_cache_key(
+ anon_map, bindparams, _unbound_option_seen=seen
+ )
+ for elem in self._to_bind
+ if elem not in seen and not seen.add(elem)
+ ),
+ )
+ if self._to_bind
+ else ()
+ )
+ + (
+ (
+ "_extra_criteria",
+ tuple(
+ elem._gen_cache_key(anon_map, bindparams)
+ for elem in self._extra_criteria
+ ),
+ )
+ if self._extra_criteria
+ else ()
+ )
+ + (
+ vis.visit_string_multi_dict(
+ "local_opts", self.local_opts, self, anon_map, bindparams
+ )
+ if self.local_opts
+ else ()
+ )
+ )
+
+ _is_chain_link = False
+
+ def _set_path_strategy(self):
+ self._to_bind.append(self)
+
+ # remove cycles; _set_path_strategy is always invoked on an
+ # anonymous clone of the Load / UnboundLoad object since #5056
+ self._to_bind = None
+
+ def _deep_clone(self, applied, process):
+ if self in applied:
+ return applied[self]
+
+ cloned = self._generate()
+
+ applied[self] = cloned
+
+ cloned.strategy = self.strategy
+
+ assert cloned.propagate_to_loaders == self.propagate_to_loaders
+ assert cloned.is_class_strategy == self.is_class_strategy
+ assert cloned.is_opts_only == self.is_opts_only
+
+ cloned._to_bind = [
+ elem._deep_clone(applied, process) for elem in self._to_bind or ()
+ ]
+
+ cloned.local_opts.update(self.local_opts)
+
+ process(cloned)
+
+ return cloned
+
+ def _apply_to_parent(self, parent, applied, bound, to_bind=None):
+ if self in applied:
+ return applied[self]
+
+ if to_bind is None:
+ to_bind = self._to_bind
+
+ cloned = self._generate()
+
+ applied[self] = cloned
+
+ cloned.strategy = self.strategy
+ if self.path:
+ attr = self.path[-1]
+ if isinstance(attr, util.string_types) and attr.endswith(
+ _DEFAULT_TOKEN
+ ):
+ attr = attr.split(":")[0] + ":" + _WILDCARD_TOKEN
+ cloned._generate_path(
+ parent.path + self.path[0:-1], attr, self.strategy, None
+ )
+
+ # these assertions can go away once the "sub options" API is
+ # mature
+ assert cloned.propagate_to_loaders == self.propagate_to_loaders
+ assert cloned.is_class_strategy == self.is_class_strategy
+ assert cloned.is_opts_only == self.is_opts_only
+
+ uniq = set()
+
+ cloned._to_bind = parent._to_bind
+
+ cloned._to_bind[:] = [
+ elem
+ for elem in cloned._to_bind
+ if elem not in uniq and not uniq.add(elem)
+ ] + [
+ elem._apply_to_parent(parent, applied, bound, to_bind)
+ for elem in to_bind
+ if elem not in uniq and not uniq.add(elem)
+ ]
+
+ cloned.local_opts.update(self.local_opts)
+
+ return cloned
+
+ def _generate_path(self, path, attr, for_strategy, wildcard_key):
+ if (
+ wildcard_key
+ and isinstance(attr, util.string_types)
+ and attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN)
+ ):
+ if attr == _DEFAULT_TOKEN:
+ self.propagate_to_loaders = False
+ attr = "%s:%s" % (wildcard_key, attr)
+ if path and _is_mapped_class(path[-1]) and not self.is_class_strategy:
+ path = path[0:-1]
+ if attr:
+ path = path + (attr,)
+ self.path = path
+ self._extra_criteria = getattr(attr, "_extra_criteria", ())
+
+ return path
+
+ def __getstate__(self):
+ d = self.__dict__.copy()
+
+ # can't pickle this right now; warning is raised by strategies
+ d["_extra_criteria"] = ()
+
+ d["path"] = self._serialize_path(self.path, filter_aliased_class=True)
+ return d
+
+ def __setstate__(self, state):
+ ret = []
+ for key in state["path"]:
+ if isinstance(key, tuple):
+ if len(key) == 2:
+ # support legacy
+ cls, propkey = key
+ of_type = None
+ else:
+ cls, propkey, of_type = key
+ prop = getattr(cls, propkey)
+ if of_type:
+ prop = prop.of_type(of_type)
+ ret.append(prop)
+ else:
+ ret.append(key)
+ state["path"] = tuple(ret)
+ self.__dict__ = state
+
+ def _process(self, compile_state, mapper_entities, raiseerr):
+ dedupes = compile_state.attributes["_unbound_load_dedupes"]
+ is_refresh = compile_state.compile_options._for_refresh_state
+ for val in self._to_bind:
+ if val not in dedupes:
+ dedupes.add(val)
+ if is_refresh and not val.propagate_to_loaders:
+ continue
+ val._bind_loader(
+ [ent.entity_zero for ent in mapper_entities],
+ compile_state.current_path,
+ compile_state.attributes,
+ raiseerr,
+ )
+
+ @classmethod
+ def _from_keys(cls, meth, keys, chained, kw):
+ opt = _UnboundLoad()
+
+ def _split_key(key):
+ if isinstance(key, util.string_types):
+ # coerce fooload('*') into "default loader strategy"
+ if key == _WILDCARD_TOKEN:
+ return (_DEFAULT_TOKEN,)
+ # coerce fooload(".*") into "wildcard on default entity"
+ elif key.startswith("." + _WILDCARD_TOKEN):
+ util.warn_deprecated(
+ "The undocumented `.{WILDCARD}` format is deprecated "
+ "and will be removed in a future version as it is "
+ "believed to be unused. "
+ "If you have been using this functionality, please "
+ "comment on Issue #4390 on the SQLAlchemy project "
+ "tracker.",
+ version="1.4",
+ )
+ key = key[1:]
+ return key.split(".")
+ else:
+ return (key,)
+
+ all_tokens = [token for key in keys for token in _split_key(key)]
+
+ for token in all_tokens[0:-1]:
+ # set _is_chain_link first so that clones of the
+ # object also inherit this flag
+ opt._is_chain_link = True
+ if chained:
+ opt = meth(opt, token, **kw)
+ else:
+ opt = opt.defaultload(token)
+
+ opt = meth(opt, all_tokens[-1], **kw)
+ opt._is_chain_link = False
+ return opt
+
+ def _chop_path(self, to_chop, path):
+ i = -1
+ for i, (c_token, (p_entity, p_prop)) in enumerate(
+ zip(to_chop, path.pairs())
+ ):
+ if isinstance(c_token, util.string_types):
+ if i == 0 and c_token.endswith(":" + _DEFAULT_TOKEN):
+ return to_chop
+ elif (
+ c_token != "relationship:%s" % (_WILDCARD_TOKEN,)
+ and c_token != p_prop.key
+ ):
+ return None
+ elif isinstance(c_token, PropComparator):
+ if c_token.property is not p_prop or (
+ c_token._parententity is not p_entity
+ and (
+ not c_token._parententity.is_mapper
+ or not c_token._parententity.isa(p_entity)
+ )
+ ):
+ return None
+ else:
+ i += 1
+
+ return to_chop[i:]
+
+ def _serialize_path(self, path, filter_aliased_class=False):
+ ret = []
+ for token in path:
+ if isinstance(token, QueryableAttribute):
+ if (
+ filter_aliased_class
+ and token._of_type
+ and inspect(token._of_type).is_aliased_class
+ ):
+ ret.append((token._parentmapper.class_, token.key, None))
+ else:
+ ret.append(
+ (
+ token._parentmapper.class_,
+ token.key,
+ token._of_type.entity if token._of_type else None,
+ )
+ )
+ elif isinstance(token, PropComparator):
+ ret.append((token._parentmapper.class_, token.key, None))
+ else:
+ ret.append(token)
+ return ret
+
+ def _bind_loader(self, entities, current_path, context, raiseerr):
+ """Convert from an _UnboundLoad() object into a Load() object.
+
+ The _UnboundLoad() uses an informal "path" and does not necessarily
+ refer to a lead entity as it may use string tokens. The Load()
+ OTOH refers to a complete path. This method reconciles from a
+ given Query into a Load.
+
+ Example::
+
+
+ query = session.query(User).options(
+ joinedload("orders").joinedload("items"))
+
+ The above options will be an _UnboundLoad object along the lines
+ of (note this is not the exact API of _UnboundLoad)::
+
+ _UnboundLoad(
+ _to_bind=[
+ _UnboundLoad(["orders"], {"lazy": "joined"}),
+ _UnboundLoad(["orders", "items"], {"lazy": "joined"}),
+ ]
+ )
+
+ After this method, we get something more like this (again this is
+ not exact API)::
+
+ Load(
+ User,
+ (User, User.orders.property))
+ Load(
+ User,
+ (User, User.orders.property, Order, Order.items.property))
+
+ """
+
+ start_path = self.path
+
+ if self.is_class_strategy and current_path:
+ start_path += (entities[0],)
+
+ # _current_path implies we're in a
+ # secondary load with an existing path
+
+ if current_path:
+ start_path = self._chop_path(start_path, current_path)
+
+ if not start_path:
+ return None
+
+ # look at the first token and try to locate within the Query
+ # what entity we are referring towards.
+ token = start_path[0]
+
+ if isinstance(token, util.string_types):
+ entity = self._find_entity_basestring(entities, token, raiseerr)
+ elif isinstance(token, PropComparator):
+ prop = token.property
+ entity = self._find_entity_prop_comparator(
+ entities, prop, token._parententity, raiseerr
+ )
+ elif self.is_class_strategy and _is_mapped_class(token):
+ entity = inspect(token)
+ if entity not in entities:
+ entity = None
+ else:
+ raise sa_exc.ArgumentError(
+ "mapper option expects " "string key or list of attributes"
+ )
+
+ if not entity:
+ return
+
+ path_element = entity
+
+ # transfer our entity-less state into a Load() object
+ # with a real entity path. Start with the lead entity
+ # we just located, then go through the rest of our path
+ # tokens and populate into the Load().
+ loader = Load(path_element)
+
+ if context is None:
+ context = loader.context
+
+ loader.strategy = self.strategy
+ loader.is_opts_only = self.is_opts_only
+ loader.is_class_strategy = self.is_class_strategy
+ loader._extra_criteria = self._extra_criteria
+
+ path = loader.path
+
+ if not loader.is_class_strategy:
+ for idx, token in enumerate(start_path):
+ if not loader._generate_path(
+ loader.path,
+ token,
+ self.strategy if idx == len(start_path) - 1 else None,
+ None,
+ raiseerr,
+ polymorphic_entity_context=context,
+ ):
+ return
+
+ loader.local_opts.update(self.local_opts)
+
+ if not loader.is_class_strategy and loader.path.has_entity:
+ effective_path = loader.path.parent
+ else:
+ effective_path = loader.path
+
+ # prioritize "first class" options over those
+ # that were "links in the chain", e.g. "x" and "y" in
+ # someload("x.y.z") versus someload("x") / someload("x.y")
+
+ if effective_path.is_token:
+ for path in effective_path.generate_for_superclasses():
+ loader._set_for_path(
+ context,
+ path,
+ replace=not self._is_chain_link,
+ merge_opts=self.is_opts_only,
+ )
+ else:
+ loader._set_for_path(
+ context,
+ effective_path,
+ replace=not self._is_chain_link,
+ merge_opts=self.is_opts_only,
+ )
+
+ return loader
+
+ def _find_entity_prop_comparator(self, entities, prop, mapper, raiseerr):
+ if _is_aliased_class(mapper):
+ searchfor = mapper
+ else:
+ searchfor = _class_to_mapper(mapper)
+ for ent in entities:
+ if orm_util._entity_corresponds_to(ent, searchfor):
+ return ent
+ else:
+ if raiseerr:
+ if not list(entities):
+ raise sa_exc.ArgumentError(
+ "Query has only expression-based entities, "
+ 'which do not apply to %s "%s"'
+ % (util.clsname_as_plain_name(type(prop)), prop)
+ )
+ else:
+ raise sa_exc.ArgumentError(
+ 'Mapped attribute "%s" does not apply to any of the '
+ "root entities in this query, e.g. %s. Please "
+ "specify the full path "
+ "from one of the root entities to the target "
+ "attribute. "
+ % (prop, ", ".join(str(x) for x in entities))
+ )
+ else:
+ return None
+
+ def _find_entity_basestring(self, entities, token, raiseerr):
+ if token.endswith(":" + _WILDCARD_TOKEN):
+ if len(list(entities)) != 1:
+ if raiseerr:
+ raise sa_exc.ArgumentError(
+ "Can't apply wildcard ('*') or load_only() "
+ "loader option to multiple entities %s. Specify "
+ "loader options for each entity individually, such "
+ "as %s."
+ % (
+ ", ".join(str(ent) for ent in entities),
+ ", ".join(
+ "Load(%s).some_option('*')" % ent
+ for ent in entities
+ ),
+ )
+ )
+ elif token.endswith(_DEFAULT_TOKEN):
+ raiseerr = False
+
+ for ent in entities:
+ # return only the first _MapperEntity when searching
+ # based on string prop name. Ideally object
+ # attributes are used to specify more exactly.
+ return ent
+ else:
+ if raiseerr:
+ raise sa_exc.ArgumentError(
+ "Query has only expression-based entities - "
+ 'can\'t find property named "%s".' % (token,)
+ )
+ else:
+ return None
+
+
+class loader_option(object):
+ def __init__(self):
+ pass
+
+ def __call__(self, fn):
+ self.name = name = fn.__name__
+ self.fn = fn
+ if hasattr(Load, name):
+ raise TypeError("Load class already has a %s method." % (name))
+ setattr(Load, name, fn)
+
+ return self
+
+ def _add_unbound_fn(self, fn):
+ self._unbound_fn = fn
+ fn_doc = self.fn.__doc__
+ self.fn.__doc__ = """Produce a new :class:`_orm.Load` object with the
+:func:`_orm.%(name)s` option applied.
+
+See :func:`_orm.%(name)s` for usage examples.
+
+""" % {
+ "name": self.name
+ }
+
+ fn.__doc__ = fn_doc
+ return self
+
+ def _add_unbound_all_fn(self, fn):
+ fn.__doc__ = """Produce a standalone "all" option for
+:func:`_orm.%(name)s`.
+
+.. deprecated:: 0.9
+
+ The :func:`_orm.%(name)s_all` function is deprecated, and will be removed
+ in a future release. Please use method chaining with
+ :func:`_orm.%(name)s` instead, as in::
+
+ session.query(MyClass).options(
+ %(name)s("someattribute").%(name)s("anotherattribute")
+ )
+
+""" % {
+ "name": self.name
+ }
+ fn = util.deprecated(
+ # This is used by `baked_lazyload_all` was only deprecated in
+ # version 1.2 so this must stick around until that is removed
+ "0.9",
+ "The :func:`.%(name)s_all` function is deprecated, and will be "
+ "removed in a future release. Please use method chaining with "
+ ":func:`.%(name)s` instead" % {"name": self.name},
+ add_deprecation_to_docstring=False,
+ )(fn)
+
+ self._unbound_all_fn = fn
+ return self
+
+
+@loader_option()
+def contains_eager(loadopt, attr, alias=None):
+ r"""Indicate that the given attribute should be eagerly loaded from
+ columns stated manually in the query.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ The option is used in conjunction with an explicit join that loads
+ the desired rows, i.e.::
+
+ sess.query(Order).\
+ join(Order.user).\
+ options(contains_eager(Order.user))
+
+ The above query would join from the ``Order`` entity to its related
+ ``User`` entity, and the returned ``Order`` objects would have the
+ ``Order.user`` attribute pre-populated.
+
+ It may also be used for customizing the entries in an eagerly loaded
+ collection; queries will normally want to use the
+ :meth:`_query.Query.populate_existing` method assuming the primary
+ collection of parent objects may already have been loaded::
+
+ sess.query(User).\
+ join(User.addresses).\
+ filter(Address.email_address.like('%@aol.com')).\
+ options(contains_eager(User.addresses)).\
+ populate_existing()
+
+ See the section :ref:`contains_eager` for complete usage details.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`contains_eager`
+
+ """
+ if alias is not None:
+ if not isinstance(alias, str):
+ info = inspect(alias)
+ alias = info.selectable
+
+ else:
+ util.warn_deprecated(
+ "Passing a string name for the 'alias' argument to "
+ "'contains_eager()` is deprecated, and will not work in a "
+ "future release. Please use a sqlalchemy.alias() or "
+ "sqlalchemy.orm.aliased() construct.",
+ version="1.4",
+ )
+
+ elif getattr(attr, "_of_type", None):
+ ot = inspect(attr._of_type)
+ alias = ot.selectable
+
+ cloned = loadopt.set_relationship_strategy(
+ attr, {"lazy": "joined"}, propagate_to_loaders=False
+ )
+ cloned.local_opts["eager_from_alias"] = alias
+ return cloned
+
+
+@contains_eager._add_unbound_fn
+def contains_eager(*keys, **kw):
+ return _UnboundLoad()._from_keys(
+ _UnboundLoad.contains_eager, keys, True, kw
+ )
+
+
+@loader_option()
+def load_only(loadopt, *attrs):
+ """Indicate that for a particular entity, only the given list
+ of column-based attribute names should be loaded; all others will be
+ deferred.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ Example - given a class ``User``, load only the ``name`` and ``fullname``
+ attributes::
+
+ session.query(User).options(load_only(User.name, User.fullname))
+
+ Example - given a relationship ``User.addresses -> Address``, specify
+ subquery loading for the ``User.addresses`` collection, but on each
+ ``Address`` object load only the ``email_address`` attribute::
+
+ session.query(User).options(
+ subqueryload(User.addresses).load_only(Address.email_address)
+ )
+
+ For a :class:`_query.Query` that has multiple entities,
+ the lead entity can be
+ specifically referred to using the :class:`_orm.Load` constructor::
+
+ session.query(User, Address).join(User.addresses).options(
+ Load(User).load_only(User.name, User.fullname),
+ Load(Address).load_only(Address.email_address)
+ )
+
+ .. note:: This method will still load a :class:`_schema.Column` even
+ if the column property is defined with ``deferred=True``
+ for the :func:`.column_property` function.
+
+ .. versionadded:: 0.9.0
+
+ """
+ cloned = loadopt.set_column_strategy(
+ attrs, {"deferred": False, "instrument": True}
+ )
+ cloned.set_column_strategy(
+ "*", {"deferred": True, "instrument": True}, {"undefer_pks": True}
+ )
+ return cloned
+
+
+@load_only._add_unbound_fn
+def load_only(*attrs):
+ return _UnboundLoad().load_only(*attrs)
+
+
+@loader_option()
+def joinedload(loadopt, attr, innerjoin=None):
+ """Indicate that the given attribute should be loaded using joined
+ eager loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ examples::
+
+ # joined-load the "orders" collection on "User"
+ query(User).options(joinedload(User.orders))
+
+ # joined-load Order.items and then Item.keywords
+ query(Order).options(
+ joinedload(Order.items).joinedload(Item.keywords))
+
+ # lazily load Order.items, but when Items are loaded,
+ # joined-load the keywords collection
+ query(Order).options(
+ lazyload(Order.items).joinedload(Item.keywords))
+
+ :param innerjoin: if ``True``, indicates that the joined eager load should
+ use an inner join instead of the default of left outer join::
+
+ query(Order).options(joinedload(Order.user, innerjoin=True))
+
+ In order to chain multiple eager joins together where some may be
+ OUTER and others INNER, right-nested joins are used to link them::
+
+ query(A).options(
+ joinedload(A.bs, innerjoin=False).
+ joinedload(B.cs, innerjoin=True)
+ )
+
+ The above query, linking A.bs via "outer" join and B.cs via "inner" join
+ would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When using
+ older versions of SQLite (< 3.7.16), this form of JOIN is translated to
+ use full subqueries as this syntax is otherwise not directly supported.
+
+ The ``innerjoin`` flag can also be stated with the term ``"unnested"``.
+ This indicates that an INNER JOIN should be used, *unless* the join
+ is linked to a LEFT OUTER JOIN to the left, in which case it
+ will render as LEFT OUTER JOIN. For example, supposing ``A.bs``
+ is an outerjoin::
+
+ query(A).options(
+ joinedload(A.bs).
+ joinedload(B.cs, innerjoin="unnested")
+ )
+
+ The above join will render as "a LEFT OUTER JOIN b LEFT OUTER JOIN c",
+ rather than as "a LEFT OUTER JOIN (b JOIN c)".
+
+ .. note:: The "unnested" flag does **not** affect the JOIN rendered
+ from a many-to-many association table, e.g. a table configured
+ as :paramref:`_orm.relationship.secondary`, to the target table; for
+ correctness of results, these joins are always INNER and are
+ therefore right-nested if linked to an OUTER join.
+
+ .. versionchanged:: 1.0.0 ``innerjoin=True`` now implies
+ ``innerjoin="nested"``, whereas in 0.9 it implied
+ ``innerjoin="unnested"``. In order to achieve the pre-1.0 "unnested"
+ inner join behavior, use the value ``innerjoin="unnested"``.
+ See :ref:`migration_3008`.
+
+ .. note::
+
+ The joins produced by :func:`_orm.joinedload` are **anonymously
+ aliased**. The criteria by which the join proceeds cannot be
+ modified, nor can the :class:`_query.Query`
+ refer to these joins in any way,
+ including ordering. See :ref:`zen_of_eager_loading` for further
+ detail.
+
+ To produce a specific SQL JOIN which is explicitly available, use
+ :meth:`_query.Query.join`.
+ To combine explicit JOINs with eager loading
+ of collections, use :func:`_orm.contains_eager`; see
+ :ref:`contains_eager`.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`joined_eager_loading`
+
+ """
+ loader = loadopt.set_relationship_strategy(attr, {"lazy": "joined"})
+ if innerjoin is not None:
+ loader.local_opts["innerjoin"] = innerjoin
+ return loader
+
+
+@joinedload._add_unbound_fn
+def joinedload(*keys, **kw):
+ return _UnboundLoad._from_keys(_UnboundLoad.joinedload, keys, False, kw)
+
+
+@loader_option()
+def subqueryload(loadopt, attr):
+ """Indicate that the given attribute should be loaded using
+ subquery eager loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ examples::
+
+ # subquery-load the "orders" collection on "User"
+ query(User).options(subqueryload(User.orders))
+
+ # subquery-load Order.items and then Item.keywords
+ query(Order).options(
+ subqueryload(Order.items).subqueryload(Item.keywords))
+
+ # lazily load Order.items, but when Items are loaded,
+ # subquery-load the keywords collection
+ query(Order).options(
+ lazyload(Order.items).subqueryload(Item.keywords))
+
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`subquery_eager_loading`
+
+ """
+ return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"})
+
+
+@subqueryload._add_unbound_fn
+def subqueryload(*keys):
+ return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {})
+
+
+@loader_option()
+def selectinload(loadopt, attr):
+ """Indicate that the given attribute should be loaded using
+ SELECT IN eager loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ examples::
+
+ # selectin-load the "orders" collection on "User"
+ query(User).options(selectinload(User.orders))
+
+ # selectin-load Order.items and then Item.keywords
+ query(Order).options(
+ selectinload(Order.items).selectinload(Item.keywords))
+
+ # lazily load Order.items, but when Items are loaded,
+ # selectin-load the keywords collection
+ query(Order).options(
+ lazyload(Order.items).selectinload(Item.keywords))
+
+ .. versionadded:: 1.2
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`selectin_eager_loading`
+
+ """
+ return loadopt.set_relationship_strategy(attr, {"lazy": "selectin"})
+
+
+@selectinload._add_unbound_fn
+def selectinload(*keys):
+ return _UnboundLoad._from_keys(_UnboundLoad.selectinload, keys, False, {})
+
+
+@loader_option()
+def lazyload(loadopt, attr):
+ """Indicate that the given attribute should be loaded using "lazy"
+ loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`lazy_loading`
+
+ """
+ return loadopt.set_relationship_strategy(attr, {"lazy": "select"})
+
+
+@lazyload._add_unbound_fn
+def lazyload(*keys):
+ return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {})
+
+
+@loader_option()
+def immediateload(loadopt, attr):
+ """Indicate that the given attribute should be loaded using
+ an immediate load with a per-attribute SELECT statement.
+
+ The load is achieved using the "lazyloader" strategy and does not
+ fire off any additional eager loaders.
+
+ The :func:`.immediateload` option is superseded in general
+ by the :func:`.selectinload` option, which performs the same task
+ more efficiently by emitting a SELECT for all loaded objects.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`selectin_eager_loading`
+
+ """
+ loader = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"})
+ return loader
+
+
+@immediateload._add_unbound_fn
+def immediateload(*keys):
+ return _UnboundLoad._from_keys(_UnboundLoad.immediateload, keys, False, {})
+
+
+@loader_option()
+def noload(loadopt, attr):
+ """Indicate that the given relationship attribute should remain unloaded.
+
+ The relationship attribute will return ``None`` when accessed without
+ producing any loading effect.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ :func:`_orm.noload` applies to :func:`_orm.relationship` attributes; for
+ column-based attributes, see :func:`_orm.defer`.
+
+ .. note:: Setting this loading strategy as the default strategy
+ for a relationship using the :paramref:`.orm.relationship.lazy`
+ parameter may cause issues with flushes, such if a delete operation
+ needs to load related objects and instead ``None`` was returned.
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ """
+
+ return loadopt.set_relationship_strategy(attr, {"lazy": "noload"})
+
+
+@noload._add_unbound_fn
+def noload(*keys):
+ return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {})
+
+
+@loader_option()
+def raiseload(loadopt, attr, sql_only=False):
+ """Indicate that the given attribute should raise an error if accessed.
+
+ A relationship attribute configured with :func:`_orm.raiseload` will
+ raise an :exc:`~sqlalchemy.exc.InvalidRequestError` upon access. The
+ typical way this is useful is when an application is attempting to ensure
+ that all relationship attributes that are accessed in a particular context
+ would have been already loaded via eager loading. Instead of having
+ to read through SQL logs to ensure lazy loads aren't occurring, this
+ strategy will cause them to raise immediately.
+
+ :func:`_orm.raiseload` applies to :func:`_orm.relationship`
+ attributes only.
+ In order to apply raise-on-SQL behavior to a column-based attribute,
+ use the :paramref:`.orm.defer.raiseload` parameter on the :func:`.defer`
+ loader option.
+
+ :param sql_only: if True, raise only if the lazy load would emit SQL, but
+ not if it is only checking the identity map, or determining that the
+ related value should just be None due to missing keys. When False, the
+ strategy will raise for all varieties of relationship loading.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+
+ .. versionadded:: 1.1
+
+ .. seealso::
+
+ :ref:`loading_toplevel`
+
+ :ref:`prevent_lazy_with_raiseload`
+
+ :ref:`deferred_raiseload`
+
+ """
+
+ return loadopt.set_relationship_strategy(
+ attr, {"lazy": "raise_on_sql" if sql_only else "raise"}
+ )
+
+
+@raiseload._add_unbound_fn
+def raiseload(*keys, **kw):
+ return _UnboundLoad._from_keys(_UnboundLoad.raiseload, keys, False, kw)
+
+
+@loader_option()
+def defaultload(loadopt, attr):
+ """Indicate an attribute should load using its default loader style.
+
+ This method is used to link to other loader options further into
+ a chain of attributes without altering the loader style of the links
+ along the chain. For example, to set joined eager loading for an
+ element of an element::
+
+ session.query(MyClass).options(
+ defaultload(MyClass.someattribute).
+ joinedload(MyOtherClass.someotherattribute)
+ )
+
+ :func:`.defaultload` is also useful for setting column-level options
+ on a related class, namely that of :func:`.defer` and :func:`.undefer`::
+
+ session.query(MyClass).options(
+ defaultload(MyClass.someattribute).
+ defer("some_column").
+ undefer("some_other_column")
+ )
+
+ .. seealso::
+
+ :meth:`_orm.Load.options` - allows for complex hierarchical
+ loader option structures with less verbosity than with individual
+ :func:`.defaultload` directives.
+
+ :ref:`relationship_loader_options`
+
+ :ref:`deferred_loading_w_multiple`
+
+ """
+ return loadopt.set_relationship_strategy(attr, None)
+
+
+@defaultload._add_unbound_fn
+def defaultload(*keys):
+ return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {})
+
+
+@loader_option()
+def defer(loadopt, key, raiseload=False):
+ r"""Indicate that the given column-oriented attribute should be deferred,
+ e.g. not loaded until accessed.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ e.g.::
+
+ from sqlalchemy.orm import defer
+
+ session.query(MyClass).options(
+ defer("attribute_one"),
+ defer("attribute_two"))
+
+ session.query(MyClass).options(
+ defer(MyClass.attribute_one),
+ defer(MyClass.attribute_two))
+
+ To specify a deferred load of an attribute on a related class,
+ the path can be specified one token at a time, specifying the loading
+ style for each link along the chain. To leave the loading style
+ for a link unchanged, use :func:`_orm.defaultload`::
+
+ session.query(MyClass).options(defaultload("someattr").defer("some_column"))
+
+ A :class:`_orm.Load` object that is present on a certain path can have
+ :meth:`_orm.Load.defer` called multiple times,
+ each will operate on the same
+ parent entity::
+
+
+ session.query(MyClass).options(
+ defaultload("someattr").
+ defer("some_column").
+ defer("some_other_column").
+ defer("another_column")
+ )
+
+ :param key: Attribute to be deferred.
+
+ :param raiseload: raise :class:`.InvalidRequestError` if the column
+ value is to be loaded from emitting SQL. Used to prevent unwanted
+ SQL from being emitted.
+
+ .. versionadded:: 1.4
+
+ .. seealso::
+
+ :ref:`deferred_raiseload`
+
+ :param \*addl_attrs: This option supports the old 0.8 style
+ of specifying a path as a series of attributes, which is now superseded
+ by the method-chained style.
+
+ .. deprecated:: 0.9 The \*addl_attrs on :func:`_orm.defer` is
+ deprecated and will be removed in a future release. Please
+ use method chaining in conjunction with defaultload() to
+ indicate a path.
+
+
+ .. seealso::
+
+ :ref:`deferred`
+
+ :func:`_orm.undefer`
+
+ """
+ strategy = {"deferred": True, "instrument": True}
+ if raiseload:
+ strategy["raiseload"] = True
+ return loadopt.set_column_strategy((key,), strategy)
+
+
+@defer._add_unbound_fn
+def defer(key, *addl_attrs, **kw):
+ if addl_attrs:
+ util.warn_deprecated(
+ "The *addl_attrs on orm.defer is deprecated. Please use "
+ "method chaining in conjunction with defaultload() to "
+ "indicate a path.",
+ version="1.3",
+ )
+ return _UnboundLoad._from_keys(
+ _UnboundLoad.defer, (key,) + addl_attrs, False, kw
+ )
+
+
+@loader_option()
+def undefer(loadopt, key):
+ r"""Indicate that the given column-oriented attribute should be undeferred,
+ e.g. specified within the SELECT statement of the entity as a whole.
+
+ The column being undeferred is typically set up on the mapping as a
+ :func:`.deferred` attribute.
+
+ This function is part of the :class:`_orm.Load` interface and supports
+ both method-chained and standalone operation.
+
+ Examples::
+
+ # undefer two columns
+ session.query(MyClass).options(undefer("col1"), undefer("col2"))
+
+ # undefer all columns specific to a single class using Load + *
+ session.query(MyClass, MyOtherClass).options(
+ Load(MyClass).undefer("*"))
+
+ # undefer a column on a related object
+ session.query(MyClass).options(
+ defaultload(MyClass.items).undefer('text'))
+
+ :param key: Attribute to be undeferred.
+
+ :param \*addl_attrs: This option supports the old 0.8 style
+ of specifying a path as a series of attributes, which is now superseded
+ by the method-chained style.
+
+ .. deprecated:: 0.9 The \*addl_attrs on :func:`_orm.undefer` is
+ deprecated and will be removed in a future release. Please
+ use method chaining in conjunction with defaultload() to
+ indicate a path.
+
+ .. seealso::
+
+ :ref:`deferred`
+
+ :func:`_orm.defer`
+
+ :func:`_orm.undefer_group`
+
+ """
+ return loadopt.set_column_strategy(
+ (key,), {"deferred": False, "instrument": True}
+ )
+
+
+@undefer._add_unbound_fn
+def undefer(key, *addl_attrs):
+ if addl_attrs:
+ util.warn_deprecated(
+ "The *addl_attrs on orm.undefer is deprecated. Please use "
+ "method chaining in conjunction with defaultload() to "
+ "indicate a path.",
+ version="1.3",
+ )
+ return _UnboundLoad._from_keys(
+ _UnboundLoad.undefer, (key,) + addl_attrs, False, {}
+ )
+
+
+@loader_option()
+def undefer_group(loadopt, name):
+ """Indicate that columns within the given deferred group name should be
+ undeferred.
+
+ The columns being undeferred are set up on the mapping as
+ :func:`.deferred` attributes and include a "group" name.
+
+ E.g::
+
+ session.query(MyClass).options(undefer_group("large_attrs"))
+
+ To undefer a group of attributes on a related entity, the path can be
+ spelled out using relationship loader options, such as
+ :func:`_orm.defaultload`::
+
+ session.query(MyClass).options(
+ defaultload("someattr").undefer_group("large_attrs"))
+
+ .. versionchanged:: 0.9.0 :func:`_orm.undefer_group` is now specific to a
+ particular entity load path.
+
+ .. seealso::
+
+ :ref:`deferred`
+
+ :func:`_orm.defer`
+
+ :func:`_orm.undefer`
+
+ """
+ return loadopt.set_column_strategy(
+ "*", None, {"undefer_group_%s" % name: True}, opts_only=True
+ )
+
+
+@undefer_group._add_unbound_fn
+def undefer_group(name):
+ return _UnboundLoad().undefer_group(name)
+
+
+@loader_option()
+def with_expression(loadopt, key, expression):
+ r"""Apply an ad-hoc SQL expression to a "deferred expression" attribute.
+
+ This option is used in conjunction with the :func:`_orm.query_expression`
+ mapper-level construct that indicates an attribute which should be the
+ target of an ad-hoc SQL expression.
+
+ E.g.::
+
+
+ sess.query(SomeClass).options(
+ with_expression(SomeClass.x_y_expr, SomeClass.x + SomeClass.y)
+ )
+
+ .. versionadded:: 1.2
+
+ :param key: Attribute to be undeferred.
+
+ :param expr: SQL expression to be applied to the attribute.
+
+ .. note:: the target attribute is populated only if the target object
+ is **not currently loaded** in the current :class:`_orm.Session`
+ unless the :meth:`_query.Query.populate_existing` method is used.
+ Please refer to :ref:`mapper_querytime_expression` for complete
+ usage details.
+
+ .. seealso::
+
+ :ref:`mapper_querytime_expression`
+
+ """
+
+ expression = coercions.expect(
+ roles.LabeledColumnExprRole, _orm_full_deannotate(expression)
+ )
+
+ return loadopt.set_column_strategy(
+ (key,), {"query_expression": True}, opts={"expression": expression}
+ )
+
+
+@with_expression._add_unbound_fn
+def with_expression(key, expression):
+ return _UnboundLoad._from_keys(
+ _UnboundLoad.with_expression, (key,), False, {"expression": expression}
+ )
+
+
+@loader_option()
+def selectin_polymorphic(loadopt, classes):
+ """Indicate an eager load should take place for all attributes
+ specific to a subclass.
+
+ This uses an additional SELECT with IN against all matched primary
+ key values, and is the per-query analogue to the ``"selectin"``
+ setting on the :paramref:`.mapper.polymorphic_load` parameter.
+
+ .. versionadded:: 1.2
+
+ .. seealso::
+
+ :ref:`polymorphic_selectin`
+
+ """
+ loadopt.set_class_strategy(
+ {"selectinload_polymorphic": True},
+ opts={
+ "entities": tuple(
+ sorted((inspect(cls) for cls in classes), key=id)
+ )
+ },
+ )
+ return loadopt
+
+
+@selectin_polymorphic._add_unbound_fn
+def selectin_polymorphic(base_cls, classes):
+ ul = _UnboundLoad()
+ ul.is_class_strategy = True
+ ul.path = (inspect(base_cls),)
+ ul.selectin_polymorphic(classes)
+ return ul