diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2017-04-07 14:18:22 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2017-04-13 14:22:59 -0400 |
| commit | b7644319e85ce38c1a576802317a9058a6aed82d (patch) | |
| tree | 12db3074d79d0c54deb247a7e79424312e183cf3 /lib/sqlalchemy | |
| parent | 755da1797432ee98dd3d1d309026a21529b45f75 (diff) | |
| download | sqlalchemy-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.py | 159 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 22 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 4 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 16 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 25 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 104 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategy_options.py | 229 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 13 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/_collections.py | 2 |
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) |
