diff options
Diffstat (limited to 'lib/sqlalchemy/orm/strategies.py')
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 255 |
1 files changed, 124 insertions, 131 deletions
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index db82f0b74..44f303fee 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -26,6 +26,7 @@ from .base import _RAISE_FOR_STATE from .base import _SET_DEFERRED_EXPIRED from .context import _column_descriptions from .context import ORMCompileState +from .context import QueryContext from .interfaces import LoaderStrategy from .interfaces import StrategizedProperty from .session import _state_session @@ -34,7 +35,6 @@ from .util import _none_set from .util import aliased from .. import event from .. import exc as sa_exc -from .. import future from .. import inspect from .. import log from .. import sql @@ -487,7 +487,7 @@ class DeferredColumnLoader(LoaderStrategy): if ( loading.load_on_ident( session, - future.select(localparent).apply_labels(), + sql.select(localparent).apply_labels(), state.key, only_load_props=group, refresh_state=state, @@ -620,7 +620,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): "_simple_lazy_clause", "_raise_always", "_raise_on_sql", - "_bakery", + "_query_cache", ) def __init__(self, parent, strategy_key): @@ -881,83 +881,68 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): for pk in self.mapper.primary_key ] - @util.preload_module("sqlalchemy.ext.baked") - def _memoized_attr__bakery(self): - return util.preloaded.ext_baked.bakery(size=50) + def _memoized_attr__query_cache(self): + return util.LRUCache(30) @util.preload_module("sqlalchemy.orm.strategy_options") def _emit_lazyload(self, session, state, primary_key_identity, passive): - # 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. - strategy_options = util.preloaded.orm_strategy_options - # 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.entity), self) - - q.add_criteria( - lambda q: q._with_invoke_all_eagers(False), self.parent_property, + stmt = sql.lambda_stmt( + lambda: sql.select(self.entity) + .apply_labels() + ._set_compile_options(ORMCompileState.default_compile_options), + global_track_bound_values=False, + lambda_cache=self._query_cache, + track_on=(self,), ) if not self.parent_property.bake_queries: - q.spoil(full=True) + stmt = stmt.spoil() + + load_options = QueryContext.default_load_options + + load_options += { + "_invoke_all_eagers": False, + "_lazy_loaded_from": state, + } if self.parent_property.secondary is not None: - q.add_criteria( - lambda q: q.select_from( - self.mapper, self.parent_property.secondary - ) + stmt += lambda stmt: stmt.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)) + stmt += lambda stmt: stmt.execution_options(autoflush=False) if 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) + opts = list(state.load_options) + + stmt += lambda stmt: stmt.options(*opts) + stmt += lambda stmt: stmt._update_compile_options( + {"_current_path": effective_path} + ) if self.use_get: if self._raise_on_sql: self._invoke_raise_load(state, passive, "raise_on_sql") - return ( - q(session) - .with_post_criteria(lambda q: q._set_lazyload_from(state)) - ._load_on_pk_identity( - session, session.query(self.mapper), primary_key_identity - ) + return loading.load_on_pk_identity( + session, + stmt, + primary_key_identity, + load_options=load_options, + execution_options={"compiled_cache": self._query_cache}, ) if self._order_by: - q.add_criteria(lambda q: q.order_by(*self._order_by)) + stmt += lambda stmt: stmt.order_by(*self._order_by) def _lazyload_reverse(compile_context): for rev in self.parent_property._reverse_property: @@ -974,13 +959,18 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): ] ).lazyload(rev.key).process_compile_state(compile_context) - q.add_criteria( - lambda q: q._add_context_option( - _lazyload_reverse, self.parent_property - ) + stmt += lambda stmt: stmt._add_context_option( + _lazyload_reverse, self.parent_property ) lazy_clause, params = self._generate_lazy_clause(state, passive) + + execution_options = { + "_sa_orm_load_options": load_options, + } + if not self.parent_property.bake_queries: + execution_options["compiled_cache"] = None + if self.key in state.dict: return attributes.ATTR_WAS_SET @@ -994,21 +984,16 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): if self._raise_on_sql: self._invoke_raise_load(state, passive, "raise_on_sql") - q.add_criteria(lambda q: q.filter(lazy_clause)) - - # set parameters in the query such that we don't overwrite - # parameters that are already set within it - def set_default_params(q): - params.update(q.load_options._params) - q.load_options += {"_params": params} - return q + stmt = stmt.add_criteria( + lambda stmt: stmt.where(lazy_clause), enable_tracking=False + ) - result = ( - q(session) - .with_post_criteria(lambda q: q._set_lazyload_from(state)) - .with_post_criteria(set_default_params) - .all() + result = session.execute( + stmt, params, future=True, execution_options=execution_options ) + + result = result.unique().scalars().all() + if self.uselist: return result else: @@ -1409,6 +1394,7 @@ class SubqueryLoader(PostLoader): "session", "execution_options", "load_options", + "params", "subq", "_data", ) @@ -1419,6 +1405,7 @@ class SubqueryLoader(PostLoader): self.session = context.session self.execution_options = context.execution_options self.load_options = context.load_options + self.params = context.params or {} self.subq = subq self._data = None @@ -1443,7 +1430,7 @@ class SubqueryLoader(PostLoader): # to work with baked query, the parameters may have been # updated since this query was created, so take these into account - rows = list(q.params(self.load_options._params)) + rows = list(q.params(self.params)) for k, v in itertools.groupby(rows, lambda x: x[1:]): self._data[k].extend(vv[0] for vv in v) @@ -1519,19 +1506,16 @@ class SubqueryLoader(PostLoader): orig_query, "orm" ) - # this would create the full blown compile state, which we don't - # need - # orig_compile_state = compile_state_cls.create_for_statement( - # orig_query, None) - if orig_query._is_lambda_element: - util.warn( - 'subqueryloader for "%s" must invoke lambda callable at %r in ' - "order to produce a new query, decreasing the efficiency " - "of caching for this statement. Consider using " - "selectinload() for more effective full-lambda caching" - % (self, orig_query) - ) + if context.load_options._lazy_loaded_from is None: + util.warn( + 'subqueryloader for "%s" must invoke lambda callable ' + "at %r in " + "order to produce a new query, decreasing the efficiency " + "of caching for this statement. Consider using " + "selectinload() for more effective full-lambda caching" + % (self, orig_query) + ) orig_query = orig_query._resolved # this is the more "quick" version, however it's not clear how @@ -2112,7 +2096,7 @@ class JoinedLoader(AbstractRelationshipLoader): else: # all other cases are innerjoin=='nested' approach eagerjoin = self._splice_nested_inner_join( - path, towrap, clauses, onclause + path, towrap, clauses, onclause, ) compile_state.eager_joins[query_entity_key] = eagerjoin @@ -2153,7 +2137,7 @@ class JoinedLoader(AbstractRelationshipLoader): assert isinstance(join_obj, orm_util._ORMJoin) elif isinstance(join_obj, sql.selectable.FromGrouping): return self._splice_nested_inner_join( - path, join_obj.element, clauses, onclause, splicing + path, join_obj.element, clauses, onclause, splicing, ) elif not isinstance(join_obj, orm_util._ORMJoin): if path[-2] is splicing: @@ -2170,12 +2154,12 @@ class JoinedLoader(AbstractRelationshipLoader): return None target_join = self._splice_nested_inner_join( - path, join_obj.right, clauses, onclause, join_obj._right_memo + path, join_obj.right, clauses, onclause, join_obj._right_memo, ) if target_join is None: right_splice = False target_join = self._splice_nested_inner_join( - path, join_obj.left, clauses, onclause, join_obj._left_memo + path, join_obj.left, clauses, onclause, join_obj._left_memo, ) if target_join is None: # should only return None when recursively called, @@ -2401,7 +2385,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): "_parent_alias", "_query_info", "_fallback_query_info", - "_bakery", + "_query_cache", ) query_info = collections.namedtuple( @@ -2504,9 +2488,8 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): (("lazy", "select"),) ).init_class_attribute(mapper) - @util.preload_module("sqlalchemy.ext.baked") - def _memoized_attr__bakery(self): - return util.preloaded.ext_baked.bakery(size=50) + def _memoized_attr__query_cache(self): + return util.LRUCache(30) def create_row_processor( self, @@ -2564,9 +2547,8 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): with_poly_entity = path_w_prop.get( context.attributes, "path_with_polymorphic", None ) - if with_poly_entity is not None: - effective_entity = with_poly_entity + effective_entity = inspect(with_poly_entity) else: effective_entity = self.entity @@ -2645,32 +2627,37 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): # we need to adapt our "pk_cols" and "in_expr" to that # entity. in non-"omit join" mode, these are against the # parent entity and do not need adaption. - insp = inspect(effective_entity) - if insp.is_aliased_class: - pk_cols = [insp._adapt_element(col) for col in pk_cols] - in_expr = insp._adapt_element(in_expr) - pk_cols = [insp._adapt_element(col) for col in pk_cols] - - q = self._bakery( - lambda session: session.query( + if effective_entity.is_aliased_class: + pk_cols = [ + effective_entity._adapt_element(col) for col in pk_cols + ] + in_expr = effective_entity._adapt_element(in_expr) + + q = sql.lambda_stmt( + lambda: sql.select( orm_util.Bundle("pk", *pk_cols), effective_entity - ), - self, + ).apply_labels(), + lambda_cache=self._query_cache, + global_track_bound_values=False, + track_on=(self, effective_entity,) + tuple(pk_cols), ) + if not self.parent_property.bake_queries: + q = q.spoil() + if not query_info.load_with_join: # the Bundle we have in the "omit_join" case is against raw, non # annotated columns, so to ensure the Query knows its primary # entity, we add it explicitly. If we made the Bundle against # annotated columns, we hit a performance issue in this specific # case, which is detailed in issue #4347. - q.add_criteria(lambda q: q.select_from(effective_entity)) + q = q.add_criteria(lambda q: q.select_from(effective_entity)) else: # in the non-omit_join case, the Bundle is against the annotated/ # mapped column of the parent entity, but the #4347 issue does not # occur in this case. pa = self._parent_alias - q.add_criteria( + q = q.add_criteria( lambda q: q.select_from(pa).join( getattr(pa, self.parent_property.key).of_type( effective_entity @@ -2678,18 +2665,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): ) ) - if query_info.load_only_child: - q.add_criteria( - lambda q: q.filter( - in_expr.in_(sql.bindparam("primary_keys", expanding=True)) - ) - ) - else: - q.add_criteria( - lambda q: q.filter( - in_expr.in_(sql.bindparam("primary_keys", expanding=True)) - ) - ) + q = q.add_criteria( + lambda q: q.filter(in_expr.in_(sql.bindparam("primary_keys"))) + ) # a test which exercises what these comments talk about is # test_selectin_relations.py -> test_twolevel_selectin_w_polymorphic @@ -2715,31 +2693,39 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): # that query will be in terms of the effective entity we were just # handed. # - # But now the selectinload/ baked query we are running is *also* + # But now the selectinload query we are running is *also* # cached. What if it's cached and running from some previous iteration # of that AliasedInsp? Well in that case it will also use the previous - # iteration of the loader options. If the baked query expires and + # iteration of the loader options. If the query expires and # gets generated again, it will be handed the current effective_entity # and the current _with_options, again in terms of whatever # compile_state.select_statement happens to be right now, so the # query will still be internally consistent and loader callables # will be correctly invoked. - q._add_lazyload_options( - orig_query._with_options, path[self.parent_property] + effective_path = path[self.parent_property] + + options = orig_query._with_options + q = q.add_criteria( + lambda q: q.options(*options)._update_compile_options( + {"_current_path": effective_path} + ) ) if context.populate_existing: - q.add_criteria(lambda q: q.populate_existing()) + q = q.add_criteria( + lambda q: q.execution_options(populate_existing=True) + ) if self.parent_property.order_by: if not query_info.load_with_join: eager_order_by = self.parent_property.order_by - if insp.is_aliased_class: + if effective_entity.is_aliased_class: eager_order_by = [ - insp._adapt_element(elem) for elem in eager_order_by + effective_entity._adapt_element(elem) + for elem in eager_order_by ] - q.add_criteria(lambda q: q.order_by(*eager_order_by)) + q = q.add_criteria(lambda q: q.order_by(*eager_order_by)) else: def _setup_outermost_orderby(compile_context): @@ -2747,7 +2733,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): util.to_list(self.parent_property.order_by) ) - q.add_criteria( + q = q.add_criteria( lambda q: q._add_context_option( _setup_outermost_orderby, self.parent_property ) @@ -2770,11 +2756,16 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): our_keys = our_keys[self._chunksize :] data = { k: v - for k, v in q(context.session).params( - primary_keys=[ - key[0] if query_info.zero_idx else key for key in chunk - ] - ) + for k, v in context.session.execute( + q, + params={ + "primary_keys": [ + key[0] if query_info.zero_idx else key + for key in chunk + ] + }, + future=True, + ).unique() } for key in chunk: @@ -2817,7 +2808,9 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): data = collections.defaultdict(list) for k, v in itertools.groupby( - q(context.session).params(primary_keys=primary_keys), + context.session.execute( + q, params={"primary_keys": primary_keys}, future=True + ).unique(), lambda x: x[0], ): data[k].extend(vv[1] for vv in v) |