summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2017-04-07 14:18:22 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2017-04-13 14:22:59 -0400
commitb7644319e85ce38c1a576802317a9058a6aed82d (patch)
tree12db3074d79d0c54deb247a7e79424312e183cf3 /lib/sqlalchemy
parent755da1797432ee98dd3d1d309026a21529b45f75 (diff)
downloadsqlalchemy-b7644319e85ce38c1a576802317a9058a6aed82d.tar.gz
Use baked lazyloading by default
The ``lazy="select"`` loader strategy now makes used of the :class:`.BakedQuery` query caching system in all cases. This removes most overhead of generating a :class:`.Query` object and running it into a :func:`.select` and then string SQL statement from the process of lazy-loading related collections and objects. The "baked" lazy loader has also been improved such that it can now cache in most cases where query load options are used. Change-Id: Ic96792fffaa045ae9aa0a4657d6d29235d3efb85 Fixes: #3954
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/ext/baked.py159
-rw-r--r--lib/sqlalchemy/orm/interfaces.py22
-rw-r--r--lib/sqlalchemy/orm/mapper.py4
-rw-r--r--lib/sqlalchemy/orm/query.py16
-rw-r--r--lib/sqlalchemy/orm/relationships.py25
-rw-r--r--lib/sqlalchemy/orm/strategies.py104
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py229
-rw-r--r--lib/sqlalchemy/orm/util.py13
-rw-r--r--lib/sqlalchemy/util/_collections.py2
9 files changed, 370 insertions, 204 deletions
diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py
index 68bd468b9..249f5db4e 100644
--- a/lib/sqlalchemy/ext/baked.py
+++ b/lib/sqlalchemy/ext/baked.py
@@ -41,10 +41,10 @@ class BakedQuery(object):
self._bakery = bakery
@classmethod
- def bakery(cls, size=200):
+ def bakery(cls, size=200, _size_alert=None):
"""Construct a new bakery."""
- _bakery = util.LRUCache(size)
+ _bakery = util.LRUCache(size, size_alert=_size_alert)
def call(initial_fn, *args):
return cls(_bakery, initial_fn, args)
@@ -134,6 +134,35 @@ class BakedQuery(object):
self._spoiled = True
return self
+ def _add_lazyload_options(self, options, effective_path):
+ """Used by per-state lazy loaders to add options to the
+ "lazy load" query from a parent query.
+
+ Creates a cache key based on given load path and query options;
+ if a repeatable cache key cannot be generated, the query is
+ "spoiled" so that it won't use caching.
+
+ """
+
+ key = ()
+
+ if effective_path.path[0].is_aliased_class:
+ # paths that are against an AliasedClass are unsafe to cache
+ # with since the AliasedClass is an ad-hoc object.
+ self.spoil()
+ else:
+ for opt in options:
+ cache_key = opt._generate_cache_key(effective_path)
+ if cache_key is False:
+ self.spoil()
+ elif cache_key is not None:
+ key += cache_key
+ self.add_criteria(
+ lambda q: q._with_current_path(effective_path).
+ _conditional_options(*options),
+ effective_path.path, key
+ )
+
def _retrieve_baked_query(self, session):
query = self._bakery.get(self._cache_key, None)
if query is None:
@@ -412,125 +441,31 @@ class Result(object):
return None
+@util.deprecated(
+ "1.2", "Baked lazy loading is now the default implementation.")
def bake_lazy_loaders():
"""Enable the use of baked queries for all lazyloaders systemwide.
- This operation should be safe for all lazy loaders, and will reduce
- Python overhead for these operations.
+ The "baked" implementation of lazy loading is now the sole implementation
+ for the base lazy loader; this method has no effect except for a warning.
"""
- BakedLazyLoader._strategy_keys[:] = []
-
- properties.RelationshipProperty.strategy_for(
- lazy="select")(BakedLazyLoader)
- properties.RelationshipProperty.strategy_for(
- lazy=True)(BakedLazyLoader)
- properties.RelationshipProperty.strategy_for(
- lazy="baked_select")(BakedLazyLoader)
-
- strategies.LazyLoader._strategy_keys[:] = BakedLazyLoader._strategy_keys[:]
+ pass
+@util.deprecated(
+ "1.2", "Baked lazy loading is now the default implementation.")
def unbake_lazy_loaders():
"""Disable the use of baked queries for all lazyloaders systemwide.
- This operation reverts the changes produced by :func:`.bake_lazy_loaders`.
+ This method now raises NotImplmentedError() as the "baked" implementation
+ is the only lazy load implementation. The
+ :paramref:`.relationship.bake_queries` flag may be used to disable
+ the caching of queries on a per-relationship basis.
"""
- strategies.LazyLoader._strategy_keys[:] = []
- BakedLazyLoader._strategy_keys[:] = []
-
- properties.RelationshipProperty.strategy_for(
- lazy="select")(strategies.LazyLoader)
- properties.RelationshipProperty.strategy_for(
- lazy=True)(strategies.LazyLoader)
- properties.RelationshipProperty.strategy_for(
- lazy="baked_select")(BakedLazyLoader)
- assert strategies.LazyLoader._strategy_keys
-
-
-@sqla_log.class_logger
-@properties.RelationshipProperty.strategy_for(lazy="baked_select")
-class BakedLazyLoader(strategies.LazyLoader):
-
- def _emit_lazyload(self, session, state, ident_key, passive):
- q = BakedQuery(
- self.mapper._compiled_cache,
- lambda session: session.query(self.mapper))
- q.add_criteria(
- lambda q: q._adapt_all_clauses()._with_invoke_all_eagers(False),
- self.parent_property)
-
- if not self.parent_property.bake_queries:
- q.spoil(full=True)
-
- if self.parent_property.secondary is not None:
- q.add_criteria(
- lambda q:
- q.select_from(self.mapper, self.parent_property.secondary))
-
- pending = not state.key
-
- # don't autoflush on pending
- if pending or passive & attributes.NO_AUTOFLUSH:
- q.add_criteria(lambda q: q.autoflush(False))
-
- if state.load_options:
- q.spoil()
- args = state.load_path[self.parent_property]
- q.add_criteria(
- lambda q:
- q._with_current_path(args), args)
- q.add_criteria(
- lambda q: q._conditional_options(*state.load_options))
-
- if self.use_get:
- return q(session)._load_on_ident(
- session.query(self.mapper), ident_key)
-
- if self.parent_property.order_by:
- q.add_criteria(
- lambda q:
- q.order_by(*util.to_list(self.parent_property.order_by)))
-
- for rev in self.parent_property._reverse_property:
- # reverse props that are MANYTOONE are loading *this*
- # object from get(), so don't need to eager out to those.
- if rev.direction is interfaces.MANYTOONE and \
- rev._use_get and \
- not isinstance(rev.strategy, strategies.LazyLoader):
-
- q.add_criteria(
- lambda q:
- q.options(
- strategy_options.Load.for_existing_path(
- q._current_path[rev.parent]
- ).baked_lazyload(rev.key)
- )
- )
-
- lazy_clause, params = self._generate_lazy_clause(state, passive)
-
- if pending:
- if orm_util._none_set.intersection(params.values()):
- return None
-
- q.add_criteria(lambda q: q.filter(lazy_clause))
- result = q(session).params(**params).all()
- if self.uselist:
- return result
- else:
- l = len(result)
- if l:
- if l > 1:
- util.warn(
- "Multiple rows returned with "
- "uselist=False for lazily-loaded attribute '%s' "
- % self.parent_property)
-
- return result[0]
- else:
- return None
+ raise NotImplementedError(
+ "Baked lazy loading is now the default implementation")
@strategy_options.loader_option()
@@ -543,12 +478,18 @@ def baked_lazyload(loadopt, attr):
@baked_lazyload._add_unbound_fn
+@util.deprecated(
+ "1.2", "Baked lazy loading is now the default "
+ "implementation for lazy loading.")
def baked_lazyload(*keys):
return strategy_options._UnboundLoad._from_keys(
strategy_options._UnboundLoad.baked_lazyload, keys, False, {})
@baked_lazyload._add_unbound_all_fn
+@util.deprecated(
+ "1.2", "Baked lazy loading is now the default "
+ "implementation for lazy loading.")
def baked_lazyload_all(*keys):
return strategy_options._UnboundLoad._from_keys(
strategy_options._UnboundLoad.baked_lazyload, keys, True, {})
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index 1b14acefb..84b5f6cc7 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -593,6 +593,28 @@ class MapperOption(object):
self.process_query(query)
+ def _generate_cache_key(self, path):
+ """Used by the baked loader to see if this option can be cached.
+
+ A given MapperOption that returns a cache key must return a key
+ that uniquely identifies the complete state of this option, which
+ will match any other MapperOption that itself retains the identical
+ state. This includes path options, flags, etc.
+
+ If the MapperOption does not apply to the given path and would
+ not affect query results on such a path, it should return None.
+
+ if the MapperOption **does** apply to the give path, however cannot
+ produce a safe cache key, it should return False; this will cancel
+ caching of the result. An unsafe cache key is one that includes
+ an ad-hoc user object, typically an AliasedClass object. As these
+ are usually created per-query, they don't work as cache keys.
+
+
+ """
+
+ return None
+
class LoaderStrategy(object):
"""Describe the loading behavior of a StrategizedProperty object.
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 28a5c5b9b..3fdba4482 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -2721,7 +2721,7 @@ class Mapper(InspectionAttr):
return util.LRUCache(self._compiled_cache_size,
size_alert=self._alert_lru_cache_limit)
- def _alert_lru_cache_limit(self):
+ def _alert_lru_cache_limit(self, lru_cache):
util.warn(
"Compiled statement cache for mapper %s is "
"reaching its size threshold of %d, based on _compiled_cache_size "
@@ -2730,7 +2730,7 @@ class Mapper(InspectionAttr):
"#faq_compiled_cache_threshold"
" for best practices." %
(self,
- self._compiled_cache.size_threshold,
+ lru_cache.size_threshold,
self._compiled_cache_size))
@_memoized_configured_property
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index bbfa393f8..272ef77fb 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -29,7 +29,8 @@ from .base import _entity_descriptor, _is_aliased_class, \
_is_mapped_class, _orm_columns, _generative, InspectionAttr
from .path_registry import PathRegistry
from .util import (
- AliasedClass, ORMAdapter, join as orm_join, with_parent, aliased
+ AliasedClass, ORMAdapter, join as orm_join, with_parent, aliased,
+ _entity_corresponds_to
)
from .. import sql, util, log, exc as sa_exc, inspect, inspection
from ..sql.expression import _interpret_as_from
@@ -3641,18 +3642,7 @@ class _MapperEntity(_QueryEntity):
return self.entity_zero
def corresponds_to(self, entity):
- if entity.is_aliased_class:
- if self.is_aliased_class:
- if entity._base_alias is self.entity_zero._base_alias:
- return True
- return False
- elif self.is_aliased_class:
- if self.entity_zero._use_mapper_path:
- return entity in self._with_polymorphic
- else:
- return entity is self.entity_zero
-
- return entity.common_parent(self.entity_zero)
+ return _entity_corresponds_to(self.entity_zero, entity)
def adapt_to_selectable(self, query, sel):
query._entities.append(self)
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 3b83f10cd..1005e7eeb 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -102,7 +102,7 @@ class RelationshipProperty(StrategizedProperty):
back_populates=None,
post_update=False,
cascade=False, extension=None,
- viewonly=False, lazy=True,
+ viewonly=False, lazy="select",
collection_class=None, passive_deletes=False,
passive_updates=True, remote_side=None,
enable_typechecks=True, join_depth=None,
@@ -277,22 +277,13 @@ class RelationshipProperty(StrategizedProperty):
:param bake_queries=True:
Use the :class:`.BakedQuery` cache to cache the construction of SQL
- used in lazy loads, when the :func:`.bake_lazy_loaders` function has
- first been called. Defaults to True and is intended to provide an
- "opt out" flag per-relationship when the baked query cache system is
- in use.
-
- .. warning::
-
- This flag **only** has an effect when the application-wide
- :func:`.bake_lazy_loaders` function has been called. It
- defaults to True so is an "opt out" flag.
-
- Setting this flag to False when baked queries are otherwise in
- use might be to reduce
- ORM memory use for this :func:`.relationship`, or to work around
- unresolved stability issues observed within the baked query
- cache system.
+ used in lazy loads. True by default. Set to False if the
+ join condition of the relationship has unusual features that
+ might not respond well to statement caching.
+
+ .. versionchanged:: 1.2
+ "Baked" loading is the default implementation for the "select",
+ a.k.a. "lazy" loading strategy for relationships.
.. versionadded:: 1.0.0
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index c70994e8f..fdcb54953 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -371,6 +371,7 @@ class NoLoader(AbstractRelationshipLoader):
@properties.RelationshipProperty.strategy_for(lazy="select")
@properties.RelationshipProperty.strategy_for(lazy="raise")
@properties.RelationshipProperty.strategy_for(lazy="raise_on_sql")
+@properties.RelationshipProperty.strategy_for(lazy="baked_select")
class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
"""Provide loading behavior for a :class:`.RelationshipProperty`
with "lazy=True", that is loads when first accessed.
@@ -380,7 +381,8 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
__slots__ = (
'_lazywhere', '_rev_lazywhere', 'use_get', '_bind_to_col',
'_equated_columns', '_rev_bind_to_col', '_rev_equated_columns',
- '_simple_lazy_clause', '_raise_always', '_raise_on_sql')
+ '_simple_lazy_clause', '_raise_always', '_raise_on_sql',
+ '_bakery')
def __init__(self, parent, strategy_key):
super(LazyLoader, self).__init__(parent, strategy_key)
@@ -575,35 +577,90 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
for pk in self.mapper.primary_key
]
- @util.dependencies("sqlalchemy.orm.strategy_options")
+ @util.dependencies("sqlalchemy.ext.baked")
+ def _memoized_attr__bakery(self, baked):
+ return baked.bakery(size=50, _size_alert=self._alert_lru_cache_limit)
+
+ def _alert_lru_cache_limit(self, lru_cache):
+ util.warn(
+ "Compiled statement cache for lazy loader on attribute %s is "
+ "reaching its size threshold of %d. Consider setting "
+ "bake_queries=False for this relationship. Please refer to "
+ "http://docs.sqlalchemy.org/en/latest/faq/performance.html"
+ "#faq_compiled_cache_threshold"
+ " for best practices." %
+ (self.parent_property,
+ lru_cache.size_threshold))
+
+ @util.dependencies(
+ "sqlalchemy.orm.strategy_options")
def _emit_lazyload(
self, strategy_options, session, state, ident_key, passive):
- q = session.query(self.mapper)._adapt_all_clauses()
- if self.parent_property.secondary is not None:
- q = q.select_from(self.mapper, self.parent_property.secondary)
+ # emit lazy load now using BakedQuery, to cut way down on the overhead
+ # of generating queries.
+ # there are two big things we are trying to guard against here:
+ #
+ # 1. two different lazy loads that need to have a different result,
+ # being cached on the same key. The results between two lazy loads
+ # can be different due to the options passed to the query, which
+ # take effect for descendant objects. Therefore we have to make
+ # sure paths and load options generate good cache keys, and if they
+ # don't, we don't cache.
+ # 2. a lazy load that gets cached on a key that includes some
+ # "throwaway" object, like a per-query AliasedClass, meaning
+ # the cache key will never be seen again and the cache itself
+ # will fill up. (the cache is an LRU cache, so while we won't
+ # run out of memory, it will perform terribly when it's full. A
+ # warning is emitted if this occurs.) We must prevent the
+ # generation of a cache key that is including a throwaway object
+ # in the key.
+
+ # note that "lazy='select'" and "lazy=True" make two separate
+ # lazy loaders. Currently the LRU cache is local to the LazyLoader,
+ # however add ourselves to the initial cache key just to future
+ # proof in case it moves
+ q = self._bakery(lambda session: session.query(self.mapper), self)
+
+ q.add_criteria(
+ lambda q: q._adapt_all_clauses()._with_invoke_all_eagers(False),
+ self.parent_property)
+
+ if not self.parent_property.bake_queries:
+ q.spoil(full=True)
- q = q._with_invoke_all_eagers(False)
+ if self.parent_property.secondary is not None:
+ q.add_criteria(
+ lambda q:
+ q.select_from(self.mapper, self.parent_property.secondary))
pending = not state.key
# don't autoflush on pending
if pending or passive & attributes.NO_AUTOFLUSH:
- q = q.autoflush(False)
-
- if state.load_path:
- q = q._with_current_path(state.load_path[self.parent_property])
+ q.add_criteria(lambda q: q.autoflush(False))
if state.load_options:
- q = q._conditional_options(*state.load_options)
+ # here, if any of the options cannot return a cache key,
+ # the BakedQuery "spoils" and caching will not occur. a path
+ # that features Cls.attribute.of_type(some_alias) will cancel
+ # caching, for example, since "some_alias" is user-defined and
+ # is usually a throwaway object.
+ effective_path = state.load_path[self.parent_property]
+ q._add_lazyload_options(
+ state.load_options, effective_path
+ )
if self.use_get:
if self._raise_on_sql:
self._invoke_raise_load(state, passive, "raise_on_sql")
- return loading.load_on_ident(q, ident_key)
+ return q(session)._load_on_ident(
+ session.query(self.mapper), ident_key)
if self.parent_property.order_by:
- q = q.order_by(*util.to_list(self.parent_property.order_by))
+ q.add_criteria(
+ lambda q:
+ q.order_by(*util.to_list(self.parent_property.order_by)))
for rev in self.parent_property._reverse_property:
# reverse props that are MANYTOONE are loading *this*
@@ -611,28 +668,31 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
if rev.direction is interfaces.MANYTOONE and \
rev._use_get and \
not isinstance(rev.strategy, LazyLoader):
- q = q.options(
- strategy_options.Load.for_existing_path(
- q._current_path[rev.parent]
- ).lazyload(rev.key)
+
+ q.add_criteria(
+ lambda q:
+ q.options(
+ strategy_options.Load.for_existing_path(
+ q._current_path[rev.parent]
+ ).lazyload(rev.key)
+ )
)
- lazy_clause, params = self._generate_lazy_clause(
- state, passive=passive)
+ lazy_clause, params = self._generate_lazy_clause(state, passive)
if pending:
if util.has_intersection(
orm_util._none_set, params.values()):
return None
+
elif util.has_intersection(orm_util._never_set, params.values()):
return None
if self._raise_on_sql:
self._invoke_raise_load(state, passive, "raise_on_sql")
- q = q.filter(lazy_clause).params(params)
-
- result = q.all()
+ q.add_criteria(lambda q: q.filter(lazy_clause))
+ result = q(session).params(**params).all()
if self.uselist:
return result
else:
diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py
index 60ed2be30..e67159b0d 100644
--- a/lib/sqlalchemy/orm/strategy_options.py
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -8,7 +8,8 @@
"""
-from .interfaces import MapperOption, PropComparator
+from .interfaces import MapperOption, PropComparator, MapperProperty
+from .attributes import QueryableAttribute
from .. import util
from ..sql.base import _generative, Generative
from .. import exc as sa_exc, inspect
@@ -82,8 +83,9 @@ class Load(Generative, MapperOption):
self.path = insp._path_registry
# note that this .context is shared among all descendant
# Load objects
- self.context = {}
+ self.context = util.OrderedDict()
self.local_opts = {}
+ self._of_type = None
@classmethod
def for_existing_path(cls, path):
@@ -91,8 +93,57 @@ class Load(Generative, MapperOption):
load.path = path
load.context = {}
load.local_opts = {}
+ load._of_type = None
return load
+ def _generate_cache_key(self, path):
+ if path.path[0].is_aliased_class:
+ return False
+
+ serialized = []
+ for (key, loader_path), obj in self.context.items():
+ if key != "loader":
+ continue
+
+ endpoint = obj._of_type or obj.path.path[-1]
+ chopped = self._chop_path(loader_path, path)
+ if not chopped and not obj._of_type:
+ continue
+
+ serialized_path = []
+
+ for token in chopped:
+ if isinstance(token, util.string_types):
+ serialized_path.append(token)
+ elif token.is_aliased_class:
+ return False
+ elif token.is_property:
+ serialized_path.append(token.key)
+ else:
+ assert token.is_mapper
+ serialized_path.append(token.class_)
+
+ if not serialized_path or endpoint != serialized_path[-1]:
+ if endpoint.is_mapper:
+ serialized_path.append(endpoint.class_)
+ elif endpoint.is_aliased_class:
+ return False
+
+ serialized.append(
+ (
+ tuple(serialized_path) +
+ obj.strategy +
+ (tuple([
+ (key, obj.local_opts[key])
+ for key in sorted(obj.local_opts)
+ ]) if obj.local_opts else ())
+ )
+ )
+ if not serialized:
+ return None
+ else:
+ return tuple(serialized)
+
def _generate(self):
cloned = super(Load, self)._generate()
cloned.local_opts = {}
@@ -119,6 +170,7 @@ class Load(Generative, MapperOption):
query._attributes.update(self.context)
def _generate_path(self, path, attr, wildcard_key, raiseerr=True):
+ self._of_type = None
if raiseerr and not path.has_entity:
if isinstance(path, TokenRegistry):
raise sa_exc.ArgumentError(
@@ -137,7 +189,9 @@ class Load(Generative, MapperOption):
self.propagate_to_loaders = False
if wildcard_key:
attr = "%s:%s" % (wildcard_key, attr)
- return path.token(attr)
+ path = path.token(attr)
+ self.path = path
+ return path
try:
# use getattr on the class to work around
@@ -171,7 +225,6 @@ class Load(Generative, MapperOption):
ac = attr._of_type
ext_info = inspect(ac)
- path_element = ext_info.mapper
existing = path.entity_path[prop].get(
self.context, "path_with_polymorphic")
if not ext_info.is_aliased_class:
@@ -182,12 +235,25 @@ class Load(Generative, MapperOption):
_existing_alias=existing)
path.entity_path[prop].set(
self.context, "path_with_polymorphic", inspect(ac))
- path = path[prop][path_element]
+
+ # the path here will go into the context dictionary and
+ # needs to match up to how the class graph is traversed.
+ # so we can't put an AliasedInsp in the path here, needs
+ # to be the base mapper.
+ path = path[prop][ext_info.mapper]
+
+ # but, we need to know what the original of_type()
+ # argument is for cache key purposes. so....store that too.
+ # it might be better for "path" to really represent,
+ # "the path", but trying to keep the impact of the cache
+ # key feature localized for now
+ self._of_type = ext_info
else:
path = path[prop]
if path.has_entity:
path = path.entity_path
+ self.path = path
return path
def __str__(self):
@@ -205,7 +271,7 @@ class Load(Generative, MapperOption):
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._generate_path(self.path, attr, "relationship")
self.strategy = strategy
if strategy is not None:
self._set_path_strategy()
@@ -215,10 +281,9 @@ class Load(Generative, MapperOption):
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._generate_path(self.path, attr, "column")
cloned.propagate_to_loaders = True
if opts:
cloned.local_opts.update(opts)
@@ -285,7 +350,7 @@ 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`.
+ object when it is passed :metfh:`.Query.options`.
This provides compatibility with the traditional system
of freestanding options, e.g. ``joinedload('x.y.z')``.
@@ -294,13 +359,30 @@ class _UnboundLoad(Load):
def __init__(self):
self.path = ()
- self._to_bind = set()
+ self._to_bind = []
self.local_opts = {}
_is_chain_link = False
+ def _generate_cache_key(self, path):
+ serialized = ()
+ for val in self._to_bind:
+ opt = val._bind_loader(
+ [path.path[0]],
+ None, None, False)
+ if opt:
+ c_key = opt._generate_cache_key(path)
+ if c_key is False:
+ return False
+ elif c_key:
+ serialized += c_key
+ if not serialized:
+ return None
+ else:
+ return serialized
+
def _set_path_strategy(self):
- self._to_bind.add(self)
+ self._to_bind.append(self)
def _generate_path(self, path, attr, wildcard_key):
if wildcard_key and isinstance(attr, util.string_types) and \
@@ -308,25 +390,28 @@ class _UnboundLoad(Load):
if attr == _DEFAULT_TOKEN:
self.propagate_to_loaders = False
attr = "%s:%s" % (wildcard_key, attr)
-
- return path + (attr, )
+ path = path + (attr, )
+ self.path = path
+ return path
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)
+ d['path'] = self._serialize_path(self.path)
return d
def __setstate__(self, state):
ret = []
for key in state['path']:
if isinstance(key, tuple):
- cls, propkey = key
- ret.append(getattr(cls, propkey))
+ if len(key) == 2:
+ # support legacy
+ cls, propkey = key
+ else:
+ cls, propkey, of_type = key
+ prop = getattr(cls, propkey)
+ if of_type:
+ prop = prop.of_type(prop)
+ ret.append(prop)
else:
ret.append(key)
state['path'] = tuple(ret)
@@ -334,7 +419,9 @@ class _UnboundLoad(Load):
def _process(self, query, raiseerr):
for val in self._to_bind:
- val._bind_loader(query, query._attributes, raiseerr)
+ val._bind_loader(
+ [ent.entity_zero for ent in query._mapper_entities],
+ query._current_path, query._attributes, raiseerr)
@classmethod
def _from_keys(cls, meth, keys, chained, kw):
@@ -384,26 +471,80 @@ class _UnboundLoad(Load):
return to_chop[i:]
- def _bind_loader(self, query, context, raiseerr):
+ def _serialize_path(self, path, reject_aliased_class=False):
+ ret = []
+ for token in path:
+ if isinstance(token, QueryableAttribute):
+ if reject_aliased_class and (
+ (token._of_type and
+ inspect(token._of_type).is_aliased_class)
+ or
+ inspect(token.parent).is_aliased_class
+ ):
+ return False
+ ret.append(
+ (token._parentmapper.class_, token.key, token._of_type))
+ 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
# _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
+ # 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(query, token, raiseerr)
+ entity = self._find_entity_basestring(entities, token, raiseerr)
elif isinstance(token, PropComparator):
prop = token.property
entity = self._find_entity_prop_comparator(
- query,
+ entities,
prop.key,
token._parententity,
raiseerr)
@@ -416,20 +557,26 @@ class _UnboundLoad(Load):
if not entity:
return
- path_element = entity.entity_zero
+ path_element = entity
# transfer our entity-less state into a Load() object
- # with a real entity path.
+ # 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)
- loader.context = context
+
+ if context is not None:
+ loader.context = context
+ else:
+ context = loader.context
+
loader.strategy = self.strategy
loader.is_opts_only = self.is_opts_only
path = loader.path
for token in start_path:
- loader.path = path = loader._generate_path(
- loader.path, token, None, raiseerr)
- if path is None:
+ if not loader._generate_path(
+ loader.path, token, None, raiseerr):
return
loader.local_opts.update(self.local_opts)
@@ -455,17 +602,19 @@ class _UnboundLoad(Load):
replace=not self._is_chain_link,
merge_opts=self.is_opts_only)
- def _find_entity_prop_comparator(self, query, token, mapper, raiseerr):
+ return loader
+
+ def _find_entity_prop_comparator(self, entities, 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):
+ for ent in entities:
+ if orm_util._entity_corresponds_to(ent, searchfor):
return ent
else:
if raiseerr:
- if not list(query._mapper_entities):
+ if not list(entities):
raise sa_exc.ArgumentError(
"Query has only expression-based entities - "
"can't find property named '%s'."
@@ -477,14 +626,14 @@ class _UnboundLoad(Load):
"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))
+ x in entities))
)
else:
return None
- def _find_entity_basestring(self, query, token, raiseerr):
+ def _find_entity_basestring(self, entities, token, raiseerr):
if token.endswith(':' + _WILDCARD_TOKEN):
- if len(list(query._mapper_entities)) != 1:
+ if len(list(entities)) != 1:
if raiseerr:
raise sa_exc.ArgumentError(
"Wildcard loader can only be used with exactly "
@@ -493,7 +642,7 @@ class _UnboundLoad(Load):
elif token.endswith(_DEFAULT_TOKEN):
raiseerr = False
- for ent in query._mapper_entities:
+ 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.
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 73b0be99c..2e61661ed 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -1039,6 +1039,19 @@ def was_deleted(object):
state = attributes.instance_state(object)
return state.was_deleted
+def _entity_corresponds_to(given, entity):
+ if entity.is_aliased_class:
+ if given.is_aliased_class:
+ if entity._base_alias is given._base_alias:
+ return True
+ return False
+ elif given.is_aliased_class:
+ if given._use_mapper_path:
+ return entity in given.with_polymorphic_mappers
+ else:
+ return entity is given
+
+ return entity.common_parent(given)
def randomize_unitofwork():
"""Use random-ordering sets within the unit of work in order
diff --git a/lib/sqlalchemy/util/_collections.py b/lib/sqlalchemy/util/_collections.py
index d94af5f62..32d9c6190 100644
--- a/lib/sqlalchemy/util/_collections.py
+++ b/lib/sqlalchemy/util/_collections.py
@@ -925,7 +925,7 @@ class LRUCache(dict):
while len(self) > self.capacity + self.capacity * self.threshold:
if size_alert:
size_alert = False
- self.size_alert()
+ self.size_alert(self)
by_counter = sorted(dict.values(self),
key=operator.itemgetter(2),
reverse=True)