# orm/strategy_options.py # Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """ """ from .interfaces import MapperOption, PropComparator from .. import util from ..sql.base import _generative, Generative from .. import exc as sa_exc, inspect from .base import _is_aliased_class, _class_to_mapper from . import util as orm_util from .path_registry import PathRegistry, TokenRegistry, \ _WILDCARD_TOKEN, _DEFAULT_TOKEN class Load(Generative, MapperOption): """Represents loader options which modify the state of a :class:`.Query` in order to affect how various mapped attributes are loaded. .. versionadded:: 0.9.0 The :meth:`.Load` system is a new foundation for the existing system of loader options, including options such as :func:`.orm.joinedload`, :func:`.orm.defer`, and others. In particular, it introduces a new method-chained system that replaces the need for dot-separated paths as well as "_all()" options such as :func:`.orm.joinedload_all`. A :class:`.Load` object can be used directly or indirectly. To use one directly, instantiate given the parent class. This style of usage is useful when dealing with a :class:`.Query` that has multiple entities, or when producing a loader option that can be applied generically to any style of query:: myopt = Load(MyClass).joinedload("widgets") The above ``myopt`` can now be used with :meth:`.Query.options`:: session.query(MyClass).options(myopt) The :class:`.Load` construct is invoked indirectly whenever one makes use of the various loader options that are present in ``sqlalchemy.orm``, including options such as :func:`.orm.joinedload`, :func:`.orm.defer`, :func:`.orm.subqueryload`, and all the rest. These constructs produce an "anonymous" form of the :class:`.Load` object which tracks attributes and options, but is not linked to a parent class until it is associated with a parent :class:`.Query`:: # produce "unbound" Load object myopt = joinedload("widgets") # when applied using options(), the option is "bound" to the # class observed in the given query, e.g. MyClass session.query(MyClass).options(myopt) Whether the direct or indirect style is used, the :class:`.Load` object returned now represents a specific "path" along the entities of a :class:`.Query`. This path can be traversed using a standard method-chaining approach. Supposing a class hierarchy such as ``User``, ``User.addresses -> Address``, ``User.orders -> Order`` and ``Order.items -> Item``, we can specify a variety of loader options along each element in the "path":: session.query(User).options( joinedload("addresses"), subqueryload("orders").joinedload("items") ) Where above, the ``addresses`` collection will be joined-loaded, the ``orders`` collection will be subquery-loaded, and within that subquery load the ``items`` collection will be joined-loaded. """ def __init__(self, entity): insp = inspect(entity) self.path = insp._path_registry self.context = {} self.local_opts = {} def _generate(self): cloned = super(Load, self)._generate() cloned.local_opts = {} return cloned strategy = None propagate_to_loaders = False def process_query(self, query): self._process(query, True) def process_query_conditionally(self, query): self._process(query, False) def _process(self, query, raiseerr): current_path = query._current_path if current_path: for (token, start_path), loader in self.context.items(): chopped_start_path = self._chop_path(start_path, current_path) if chopped_start_path is not None: query._attributes[(token, chopped_start_path)] = loader else: query._attributes.update(self.context) def _generate_path(self, path, attr, wildcard_key, raiseerr=True): 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( "Attribute '%s' of entity '%s' does not " "refer to a mapped entity" % (path.prop.key, path.parent.entity) ) if isinstance(attr, util.string_types): default_token = attr.endswith(_DEFAULT_TOKEN) 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) return path.token(attr) try: # use getattr on the class to work around # synonyms, hybrids, etc. attr = getattr(path.entity.class_, attr) except AttributeError: if raiseerr: raise sa_exc.ArgumentError( "Can't find property named '%s' on the " "mapped entity %s in this Query. " % ( attr, path.entity) ) else: return None else: attr = attr.property path = path[attr] else: prop = attr.property if not prop.parent.common_parent(path.mapper): if raiseerr: raise sa_exc.ArgumentError("Attribute '%s' does not " "link from element '%s'" % (attr, path.entity)) else: return None if getattr(attr, '_of_type', None): ac = attr._of_type ext_info = inspect(ac) path_element = ext_info.mapper 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) path.entity_path[prop].set(self.context, "path_with_polymorphic", inspect(ac)) path = path[prop][path_element] else: path = path[prop] if path.has_entity: path = path.entity_path return path def _coerce_strat(self, strategy): if strategy is not None: strategy = tuple(sorted(strategy.items())) return strategy @_generative def set_relationship_strategy(self, attr, strategy, propagate_to_loaders=True): strategy = self._coerce_strat(strategy) self.propagate_to_loaders = propagate_to_loaders # if the path is a wildcard, this will set propagate_to_loaders=False self.path = self._generate_path(self.path, attr, "relationship") self.strategy = strategy if strategy is not None: self._set_path_strategy() @_generative def set_column_strategy(self, attrs, strategy, opts=None): strategy = self._coerce_strat(strategy) for attr in attrs: path = self._generate_path(self.path, attr, "column") cloned = self._generate() cloned.strategy = strategy cloned.path = path cloned.propagate_to_loaders = True if opts: cloned.local_opts.update(opts) cloned._set_path_strategy() def _set_path_strategy(self): if self.path.has_entity: self.path.parent.set(self.context, "loader", self) else: self.path.set(self.context, "loader", self) def __getstate__(self): d = self.__dict__.copy() d["path"] = self.path.serialize() return d def __setstate__(self, state): self.__dict__.update(state) self.path = PathRegistry.deserialize(self.path) 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 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:`.Load` object when it is passed :meth:`.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 = set() self.local_opts = {} _is_chain_link = False def _set_path_strategy(self): self._to_bind.add(self) def _generate_path(self, path, attr, 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) return path + (attr, ) def __getstate__(self): d = self.__dict__.copy() d['path'] = ret = [] for token in util.to_list(self.path): if isinstance(token, PropComparator): ret.append((token._parentmapper.class_, token.key)) else: ret.append(token) return d def __setstate__(self, state): ret = [] for key in state['path']: if isinstance(key, tuple): cls, propkey = key ret.append(getattr(cls, propkey)) else: ret.append(key) state['path'] = tuple(ret) self.__dict__ = state def _process(self, query, raiseerr): for val in self._to_bind: val._bind_loader(query, query._attributes, raiseerr) @classmethod def _from_keys(self, 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): 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]: if chained: opt = meth(opt, token, **kw) else: opt = opt.defaultload(token) opt._is_chain_link = True 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_mapper, 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: return None else: i += 1 return to_chop[i:] def _bind_loader(self, query, context, raiseerr): start_path = self.path # _current_path implies we're in a # secondary load with an existing path current_path = query._current_path if current_path: start_path = self._chop_path(start_path, current_path) if not start_path: return None token = start_path[0] if isinstance(token, util.string_types): entity = self._find_entity_basestring(query, token, raiseerr) elif isinstance(token, PropComparator): prop = token.property entity = self._find_entity_prop_comparator( query, prop.key, token._parententity, raiseerr) else: raise sa_exc.ArgumentError( "mapper option expects " "string key or list of attributes") if not entity: return path_element = entity.entity_zero # transfer our entity-less state into a Load() object # with a real entity path. loader = Load(path_element) loader.context = context loader.strategy = self.strategy path = loader.path for token in start_path: loader.path = path = loader._generate_path( loader.path, token, None, raiseerr) if path is None: return loader.local_opts.update(self.local_opts) if 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 self._is_chain_link: effective_path.setdefault(context, "loader", loader) else: effective_path.set(context, "loader", loader) def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): if _is_aliased_class(mapper): searchfor = mapper else: searchfor = _class_to_mapper(mapper) for ent in query._mapper_entities: if ent.corresponds_to(searchfor): return ent else: if raiseerr: if not list(query._mapper_entities): raise sa_exc.ArgumentError( "Query has only expression-based entities - " "can't find property named '%s'." % (token, ) ) else: raise sa_exc.ArgumentError( "Can't find property '%s' on any entity " "specified in this Query. Note the full path " "from root (%s) to target entity must be specified." % (token, ",".join(str(x) for x in query._mapper_entities)) ) else: return None def _find_entity_basestring(self, query, token, raiseerr): if token.endswith(':' + _WILDCARD_TOKEN): if len(list(query._mapper_entities)) != 1: if raiseerr: raise sa_exc.ArgumentError( "Wildcard loader can only be used with exactly " "one entity. Use Load(ent) to specify " "specific entities.") elif token.endswith(_DEFAULT_TOKEN): raiseerr = False for ent in query._mapper_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:`.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): self._unbound_all_fn = fn fn.__doc__ = """Produce a standalone "all" option for :func:`.orm.%(name)s`. .. deprecated:: 0.9.0 The "_all()" style is replaced by method chaining, e.g.:: session.query(MyClass).options( %(name)s("someattribute").%(name)s("anotherattribute") ) """ % {"name": self.name} return self @loader_option() def contains_eager(loadopt, attr, alias=None): """Indicate that the given attribute should be eagerly loaded from columns stated manually in the query. This function is part of the :class:`.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. :func:`contains_eager` also accepts an `alias` argument, which is the string name of an alias, an :func:`~sqlalchemy.sql.expression.alias` construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when the eagerly-loaded rows are to come from an aliased table:: user_alias = aliased(User) sess.query(Order).\\ join((user_alias, Order.user)).\\ options(contains_eager(Order.user, alias=user_alias)) .. seealso:: :ref:`contains_eager` """ if alias is not None: if not isinstance(alias, str): info = inspect(alias) alias = info.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:`.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("name", "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("addreses").load_only("email_address") ) For a :class:`.Query` that has multiple entities, the lead entity can be specifically referred to using the :class:`.Load` constructor:: session.query(User, Address).join(User.addresses).options( Load(User).load_only("name", "fullname"), Load(Address).load_only("email_addres") ) .. versionadded:: 0.9.0 """ cloned = loadopt.set_column_strategy( attrs, {"deferred": False, "instrument": True} ) cloned.set_column_strategy("*", {"deferred": True, "instrument": 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:`.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)) If the joined-eager load is chained onto an existing LEFT OUTER JOIN, ``innerjoin=True`` will be bypassed and the join will continue to chain as LEFT OUTER JOIN so that the results don't change. As an alternative, specify the value ``"nested"``. This will instead nest the join on the right side, e.g. using the form "a LEFT OUTER JOIN (b JOIN c)". .. versionadded:: 0.9.4 Added ``innerjoin="nested"`` option to support nesting of eager "inner" joins. .. 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` refer to these joins in any way, including ordering. To produce a specific SQL JOIN which is explicitly available, use :meth:`.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:`contains_eager` :func:`.orm.subqueryload` :func:`.orm.lazyload` :paramref:`.relationship.lazy` :paramref:`.relationship.innerjoin` - :func:`.relationship`-level version of the :paramref:`.joinedload.innerjoin` option. """ 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) @joinedload._add_unbound_all_fn def joinedload_all(*keys, **kw): return _UnboundLoad._from_keys( _UnboundLoad.joinedload, keys, True, 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:`.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` :func:`.orm.joinedload` :func:`.orm.lazyload` :paramref:`.relationship.lazy` """ return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"}) @subqueryload._add_unbound_fn def subqueryload(*keys): return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {}) @subqueryload._add_unbound_all_fn def subqueryload_all(*keys): return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, True, {}) @loader_option() def lazyload(loadopt, attr): """Indicate that the given attribute should be loaded using "lazy" loading. This function is part of the :class:`.Load` interface and supports both method-chained and standalone operation. .. seealso:: :paramref:`.relationship.lazy` """ return loadopt.set_relationship_strategy(attr, {"lazy": "select"}) @lazyload._add_unbound_fn def lazyload(*keys): return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {}) @lazyload._add_unbound_all_fn def lazyload_all(*keys): return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, True, {}) @loader_option() def immediateload(loadopt, attr): """Indicate that the given attribute should be loaded using an immediate load with a per-attribute SELECT statement. This function is part of the :class:`.Load` interface and supports both method-chained and standalone operation. .. seealso:: :ref:`loading_toplevel` :func:`.orm.joinedload` :func:`.orm.lazyload` :paramref:`.relationship.lazy` """ 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. This function is part of the :class:`.Load` interface and supports both method-chained and standalone operation. :func:`.orm.noload` applies to :func:`.relationship` attributes; for column-based attributes, see :func:`.orm.defer`. """ 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 defaultload(loadopt, attr): """Indicate an attribute should load using its default loader style. This method is used to link to other loader options, such as to set the :func:`.orm.defer` option on a class that is linked to a relationship of the parent class being loaded, :func:`.orm.defaultload` can be used to navigate this path without changing the loading style of the relationship:: session.query(MyClass).options(defaultload("someattr").defer("some_column")) .. seealso:: :func:`.orm.defer` :func:`.orm.undefer` """ 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): """Indicate that the given column-oriented attribute should be deferred, e.g. not loaded until accessed. This function is part of the :class:`.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:`.Load` object that is present on a certain path can have :meth:`.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 \*addl_attrs: Deprecated; 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. .. seealso:: :ref:`deferred` :func:`.orm.undefer` """ return loadopt.set_column_strategy( (key, ), {"deferred": True, "instrument": True} ) @defer._add_unbound_fn def defer(key, *addl_attrs): return _UnboundLoad._from_keys(_UnboundLoad.defer, (key, ) + addl_attrs, False, {}) @loader_option() def undefer(loadopt, key): """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:`.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("*")) :param key: Attribute to be undeferred. :param \*addl_attrs: Deprecated; 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. .. 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): 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 particiular entity load path. .. seealso:: :ref:`deferred` :func:`.orm.defer` :func:`.orm.undefer` """ return loadopt.set_column_strategy( "*", None, {"undefer_group": name} ) @undefer_group._add_unbound_fn def undefer_group(name): return _UnboundLoad().undefer_group(name)