From b8ff111975be2b8d2e370f51168e39c7fae44e92 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 8 Jun 2021 15:43:13 -0400 Subject: [PATCH] simplify relationship caching options Clarified the current purpose of the :paramref:`_orm.relationship.bake_queries` flag, which in 1.4 is to enable or disable "lambda caching" of statements within the "lazyload" and "selectinload" loader strategies; this is separate from the more foundational SQL query cache that is used for most statements. Additionally, the lazy loader no longer uses its own cache for many-to-one SQL queries, which was an implementation quirk that doesn't exist for any other loader scenario. Finally, the "lru cache" warning that the lazyloader and selectinloader strategies could emit when handling a wide array of class/relationship combinations has been removed; based on analysis of some end-user cases, this warning doesn't suggest any significant issue. While setting ``bake_queries=False`` for such a relationship will remove this cache from being used, there's no particular performance gain in this case as using no caching vs. using a cache that needs to refresh often likely still wins out on the caching being used side. Fixes: #6072 Fixes: #6487 Change-Id: Ida61f09b837d3acdafa07344d7d747d7f3ab226a --- doc/build/changelog/unreleased_14/6072.rst | 20 +++++++++++ lib/sqlalchemy/orm/relationships.py | 26 +++++++------- lib/sqlalchemy/orm/strategies.py | 38 ++++++++------------ test/aaa_profiling/test_orm.py | 2 +- test/orm/test_cache_key.py | 4 +-- test/orm/test_subquery_relations.py | 41 ---------------------- 6 files changed, 51 insertions(+), 80 deletions(-) create mode 100644 doc/build/changelog/unreleased_14/6072.rst diff --git a/doc/build/changelog/unreleased_14/6072.rst b/doc/build/changelog/unreleased_14/6072.rst new file mode 100644 index 0000000000..29e3294c11 --- /dev/null +++ b/doc/build/changelog/unreleased_14/6072.rst @@ -0,0 +1,20 @@ +.. change:: + :tags: bug, orm + :tickets: 6072, 6487 + + Clarified the current purpose of the + :paramref:`_orm.relationship.bake_queries` flag, which in 1.4 is to enable + or disable "lambda caching" of statements within the "lazyload" and + "selectinload" loader strategies; this is separate from the more + foundational SQL query cache that is used for most statements. + Additionally, the lazy loader no longer uses its own cache for many-to-one + SQL queries, which was an implementation quirk that doesn't exist for any + other loader scenario. Finally, the "lru cache" warning that the lazyloader + and selectinloader strategies could emit when handling a wide array of + class/relationship combinations has been removed; based on analysis of some + end-user cases, this warning doesn't suggest any significant issue. While + setting ``bake_queries=False`` for such a relationship will remove this + cache from being used, there's no particular performance gain in this case + as using no caching vs. using a cache that needs to refresh often likely + still wins out on the caching being used side. + diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 2d1ed6ced8..89cc37de59 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -364,20 +364,20 @@ class RelationshipProperty(StrategizedProperty): :ref:`error_qzyx` - usage example :param bake_queries=True: - Use the :class:`.BakedQuery` cache to cache the construction of SQL - 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 - - .. seealso:: + Enable :ref:`lambda caching `_ for loader + strategies, if applicable, which adds a performance gain to the + construction of SQL constructs used by loader strategies, in addition + to the usual SQL statement caching used throughout SQLAlchemy. This + parameter currently applies only to the "lazy" and "selectin" loader + strategies. There is generally no reason to set this parameter to + False. - :ref:`baked_toplevel` + .. versionchanged:: 1.4 Relationship loaders no longer use the + previous "baked query" system of query caching. The "lazy" + and "selectin" loaders make use of the "lambda cache" system + for the construction of SQL constructs, + as well as the usual SQL caching system that is throughout + SQLAlchemy as of the 1.4 series. :param cascade: A comma-separated list of cascade rules which determines how diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index dca45730c5..4a8ebaabb4 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -547,9 +547,6 @@ class AbstractRelationshipLoader(LoaderStrategy): self.target = self.parent_property.target self.uselist = self.parent_property.uselist - def _size_alert(self, lru_cache): - util.warn("LRU cache size alert for loader strategy: %s" % self) - @log.class_logger @relationships.RelationshipProperty.strategy_for(do_nothing=True) @@ -630,7 +627,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): "_simple_lazy_clause", "_raise_always", "_raise_on_sql", - "_query_cache", + "_lambda_cache", ) def __init__(self, parent, strategy_key): @@ -899,12 +896,12 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): for pk in self.mapper.primary_key ] - def _memoized_attr__query_cache(self): - # cache is per lazy loader; stores not only cached SQL but also + def _memoized_attr__lambda_cache(self): + # cache is per lazy loader, and is used for caching of # sqlalchemy.sql.lambdas.AnalyzedCode and # sqlalchemy.sql.lambdas.AnalyzedFunction objects which are generated # from the StatementLambda used. - return util.LRUCache(30, size_alert=self._size_alert) + return util.LRUCache(30) @util.preload_module("sqlalchemy.orm.strategy_options") def _emit_lazyload( @@ -923,7 +920,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL) ._set_compile_options(ORMCompileState.default_compile_options), global_track_bound_values=False, - lambda_cache=self._query_cache, + lambda_cache=self._lambda_cache, track_on=(self,), ) @@ -979,11 +976,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): self._invoke_raise_load(state, passive, "raise_on_sql") return loading.load_on_pk_identity( - session, - stmt, - primary_key_identity, - load_options=load_options, - execution_options={"compiled_cache": self._query_cache}, + session, stmt, primary_key_identity, load_options=load_options ) if self._order_by: @@ -1018,8 +1011,6 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): 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 @@ -1592,10 +1583,7 @@ class SubqueryLoader(PostLoader): q = self.subq assert q.session is None - if "compiled_cache" in self.execution_options: - q = q.execution_options( - compiled_cache=self.execution_options["compiled_cache"] - ) + q = q.with_session(self.session) if self.load_options._populate_existing: @@ -2626,7 +2614,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): "_parent_alias", "_query_info", "_fallback_query_info", - "_query_cache", + "_lambda_cache", ) query_info = collections.namedtuple( @@ -2730,8 +2718,12 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): (("lazy", "select"),) ).init_class_attribute(mapper) - def _memoized_attr__query_cache(self): - return util.LRUCache(30, size_alert=self._size_alert) + def _memoized_attr__lambda_cache(self): + # cache is per lazy loader, and is used for caching of + # sqlalchemy.sql.lambdas.AnalyzedCode and + # sqlalchemy.sql.lambdas.AnalyzedFunction objects which are generated + # from the StatementLambda used. + return util.LRUCache(30) def create_row_processor( self, @@ -2879,7 +2871,7 @@ class SelectInLoader(PostLoader, util.MemoizedSlots): "plugin_subject": effective_entity, } ), - lambda_cache=self._query_cache, + lambda_cache=self._lambda_cache, global_track_bound_values=False, track_on=(self, effective_entity) + (tuple(pk_cols),), ) diff --git a/test/aaa_profiling/test_orm.py b/test/aaa_profiling/test_orm.py index 356ea252d8..7de1811aed 100644 --- a/test/aaa_profiling/test_orm.py +++ b/test/aaa_profiling/test_orm.py @@ -155,7 +155,7 @@ class MergeTest(NoCache, fixtures.MappedTest): self.assert_sql_count(testing.db, go2, 2) -class LoadManyToOneFromIdentityTest(NoCache, fixtures.MappedTest): +class LoadManyToOneFromIdentityTest(fixtures.MappedTest): """test overhead associated with many-to-one fetches. diff --git a/test/orm/test_cache_key.py b/test/orm/test_cache_key.py index 390fc514d3..67f2d02306 100644 --- a/test/orm/test_cache_key.py +++ b/test/orm/test_cache_key.py @@ -712,9 +712,9 @@ class RoundTripTest(QueryTest, AssertsCompiledSQL): sess = Session(connection) with mock.patch( - "sqlalchemy.orm.strategies.LazyLoader._query_cache", cache + "sqlalchemy.orm.strategies.LazyLoader._lambda_cache", cache ), mock.patch( - "sqlalchemy.orm.strategies.SelectInLoader._query_cache", cache + "sqlalchemy.orm.strategies.SelectInLoader._lambda_cache", cache ): def go(): diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py index 96106b49df..512ec0fe2b 100644 --- a/test/orm/test_subquery_relations.py +++ b/test/orm/test_subquery_relations.py @@ -89,47 +89,6 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): self.assert_sql_count(testing.db, go, 2) - def test_query_is_cached(self): - users, Address, addresses, User = ( - self.tables.users, - self.classes.Address, - self.tables.addresses, - self.classes.User, - ) - - mapper( - User, - users, - properties={ - "addresses": relationship( - mapper(Address, addresses), - lazy="subquery", - order_by=Address.id, - ) - }, - ) - query_cache = {} - sess = fixture_session() - - def go(): - sess.close() - - stmt = select(User).filter(User.id == 7) - - sess.execute( - stmt, execution_options={"compiled_cache": query_cache} - ).one() - - for i in range(3): - go() - - qclen = len(query_cache) - - for i in range(5): - go() - - eq_(len(query_cache), qclen) - def test_params_arent_cached(self): users, Address, addresses, User = ( self.tables.users, -- 2.47.2