From: Mike Bayer Date: Tue, 13 Apr 2021 03:48:02 +0000 (-0400) Subject: Disable raiseload in immediateload strategy X-Git-Tag: rel_1_4_8~10 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=d5f0e1d846fd3cf29e88989db362348796179c3d;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Disable raiseload in immediateload strategy Fixed issue in the new feature of :meth:`_orm.Session.refresh` introduced by :ticket:`1763` where eagerly loaded relationships are also refreshed, where the ``lazy="raise"`` and ``lazy="raise_on_sql"`` loader strategies would interfere with the :func:`_orm.immediateload` loader strategy, thus breaking the feature for relationships that were loaded with :func:`_orm.selectinload`, :func:`_orm.subqueryload` as well. Also update some docs re: refresh, populate existing, etc. Fixes: #6252 Change-Id: I5ac1430d33f1ce868426c22c7635f41f738580ce --- diff --git a/doc/build/changelog/unreleased_14/6252.rst b/doc/build/changelog/unreleased_14/6252.rst new file mode 100644 index 0000000000..6a087546b6 --- /dev/null +++ b/doc/build/changelog/unreleased_14/6252.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, orm + :tickets: 6252 + + Fixed issue in the new feature of :meth:`_orm.Session.refresh` introduced + by :ticket:`1763` where eagerly loaded relationships are also refreshed, + where the ``lazy="raise"`` and ``lazy="raise_on_sql"`` loader strategies + would interfere with the :func:`_orm.immediateload` loader strategy, thus + breaking the feature for relationships that were loaded with + :func:`_orm.selectinload`, :func:`_orm.subqueryload` as well. diff --git a/doc/build/orm/queryguide.rst b/doc/build/orm/queryguide.rst index 71893b81e8..58c278a1d3 100644 --- a/doc/build/orm/queryguide.rst +++ b/doc/build/orm/queryguide.rst @@ -827,6 +827,20 @@ model of a highly isolated transaction, and to the degree that data is expected to change within the transaction outside of the local changes being made, those use cases would be handled using explicit steps such as this method. +Using ``populate_existing``, any set of objects that matches a query +can be refreshed, and it also allows control over relationship loader options. +E.g. to refresh an instance while also refreshing a related set of objects:: + + stmt = ( + select(User). + where(User.name.in_(names)). + execution_options(populate_existing=True). + options(selectinload(User.addresses) + ) + # will refresh all matching User objects as well as the related + # Address objects + users = session.execute(stmt).scalars().all() + Another use case for ``populate_existing`` is in support of various attribute loading features that can change how an attribute is loaded on a per-query basis. Options for which this apply include: diff --git a/doc/build/orm/session_state_management.rst b/doc/build/orm/session_state_management.rst index b91ffadb40..d521d19e0e 100644 --- a/doc/build/orm/session_state_management.rst +++ b/doc/build/orm/session_state_management.rst @@ -525,13 +525,25 @@ be that of a column-mapped attribute:: # reload obj1.attr1, obj1.attr2 session.refresh(obj1, ['attr1', 'attr2']) -An alternative method of refreshing which is often more flexible is to -use the :meth:`_orm.Query.populate_existing` method of :class:`_orm.Query`. -With this option, all of the ORM objects returned by the :class:`_orm.Query` -will be refreshed with the contents of what was loaded in the SELECT:: +.. tip:: - for user in session.query(User).populate_existing().filter(User.name.in_(['a', 'b', 'c'])): - print(user) # will be refreshed for those columns that came back from the query + An alternative method of refreshing which is often more flexible is to + use the :ref:`orm_queryguide_populate_existing` feature of the ORM, + available for :term:`2.0 style` queries with :func:`_sql.select` as well + as from the :meth:`_orm.Query.populate_existing` method of :class:`_orm.Query` + within :term:`1.x style` queries. Using this execution option, + all of the ORM objects returned in the result set of the statement + will be refreshed with data from the database:: + + stmt = ( + select(User). + execution_options(populate_existing=True). + where((User.name.in_(['a', 'b', 'c'])) + ) + for user in session.execute(stmt).scalars(): + print(user) # will be refreshed for those columns that came back from the query + + See :ref:`orm_queryguide_populate_existing` for further detail. What Actually Loads @@ -640,7 +652,8 @@ transactions, an understanding of the isolation behavior in effect is essential. :meth:`.Session.refresh` - :meth:`_orm.Query.populate_existing` - :class:`_orm.Query` method that refreshes + :ref:`orm_queryguide_populate_existing` - allows any ORM query + to refresh objects as they would be loaded normally, refreshing all matching objects in the identity map against the results of a SELECT statement. diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index d08abb7880..d5f418b1ca 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -2161,24 +2161,39 @@ class Session(_SessionClassMethods): util.raise_(e, with_traceback=sys.exc_info()[2]) def refresh(self, instance, attribute_names=None, with_for_update=None): - """Expire and refresh the attributes on the given instance. - - A query will be issued to the database and all attributes will be - refreshed with their current database value. - - Lazy-loaded relational attributes will remain lazily loaded, so that - the instance-wide refresh operation will be followed immediately by - the lazy load of that attribute. - - Eagerly-loaded relational attributes will eagerly load within the - single refresh operation. + """Expire and refresh attributes on the given instance. + + The selected attributes will first be expired as they would when using + :meth:`_orm.Session.expire`; then a SELECT statement will be issued to + the database to refresh column-oriented attributes with the current + value available in the current transaction. + + :func:`_orm.relationship` oriented attributes will also be immediately + loaded if they were already eagerly loaded on the object, using the + same eager loading strategy that they were loaded with originally. + Unloaded relationship attributes will remain unloaded, as will + relationship attributes that were originally lazy loaded. + + .. versionadded:: 1.4 - the :meth:`_orm.Session.refresh` method + can also refresh eagerly loaded attributes. + + .. tip:: + + While the :meth:`_orm.Session.refresh` method is capable of + refreshing both column and relationship oriented attributes, its + primary focus is on refreshing of local column-oriented attributes + on a single instance. For more open ended "refresh" functionality, + including the ability to refresh the attributes on many objects at + once while having explicit control over relationship loader + strategies, use the + :ref:`populate existing ` feature + instead. Note that a highly isolated transaction will return the same values as were previously read in that same transaction, regardless of changes - in database state outside of that transaction - usage of - :meth:`~Session.refresh` usually only makes sense if non-ORM SQL - statement were emitted in the ongoing transaction, or if autocommit - mode is turned on. + in database state outside of that transaction. Refreshing + attributes usually only makes sense at the start of a transaction + where database rows have not yet been accessed. :param attribute_names: optional. An iterable collection of string attribute names indicating a subset of attributes to @@ -2191,8 +2206,6 @@ class Session(_SessionClassMethods): :meth:`_query.Query.with_for_update`. Supersedes the :paramref:`.Session.refresh.lockmode` parameter. - .. versionadded:: 1.2 - .. seealso:: :ref:`session_expire` - introductory material @@ -2201,7 +2214,8 @@ class Session(_SessionClassMethods): :meth:`.Session.expire_all` - :meth:`_orm.Query.populate_existing` + :ref:`orm_queryguide_populate_existing` - allows any ORM query + to refresh objects as they would be loaded normally. """ try: diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index da8e0439fe..efaf77a4f1 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -972,7 +972,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): ) if use_get: - if self._raise_on_sql: + if self._raise_on_sql and not passive & attributes.NO_RAISE: self._invoke_raise_load(state, passive, "raise_on_sql") return loading.load_on_pk_identity( @@ -1028,7 +1028,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): elif util.has_intersection(orm_util._never_set, params.values()): return None - if self._raise_on_sql: + if self._raise_on_sql and not passive & attributes.NO_RAISE: self._invoke_raise_load(state, passive, "raise_on_sql") stmt = stmt.add_criteria( @@ -1234,7 +1234,9 @@ class ImmediateLoader(PostLoader): populators, ): def load_immediate(state, dict_, row): - state.get_impl(self.key).get(state, dict_) + state.get_impl(self.key).get( + state, dict_, attributes.PASSIVE_OFF | attributes.NO_RAISE + ) if self._check_recursive_postload(context, path): return diff --git a/test/orm/test_immediate_load.py b/test/orm/test_immediate_load.py index 7efd3436c1..0e166950e6 100644 --- a/test/orm/test_immediate_load.py +++ b/test/orm/test_immediate_load.py @@ -1,5 +1,6 @@ """basic tests of lazy loaded attributes""" +from sqlalchemy import testing from sqlalchemy.orm import immediateload from sqlalchemy.orm import mapper from sqlalchemy.orm import relationship @@ -12,7 +13,13 @@ class ImmediateTest(_fixtures.FixtureTest): run_inserts = "once" run_deletes = None - def test_basic_option(self): + @testing.combinations( + ("raise",), + ("raise_on_sql",), + ("select",), + ("immediate"), + ) + def test_basic_option(self, default_lazy): Address, addresses, users, User = ( self.classes.Address, self.tables.addresses, @@ -21,7 +28,11 @@ class ImmediateTest(_fixtures.FixtureTest): ) mapper(Address, addresses) - mapper(User, users, properties={"addresses": relationship(Address)}) + mapper( + User, + users, + properties={"addresses": relationship(Address, lazy=default_lazy)}, + ) sess = fixture_session() result = ( @@ -44,6 +55,43 @@ class ImmediateTest(_fixtures.FixtureTest): result, ) + @testing.combinations( + ("raise",), + ("raise_on_sql",), + ("select",), + ("immediate"), + ) + def test_basic_option_m2o(self, default_lazy): + Address, addresses, users, User = ( + self.classes.Address, + self.tables.addresses, + self.tables.users, + self.classes.User, + ) + + mapper( + Address, + addresses, + properties={"user": relationship(User, lazy=default_lazy)}, + ) + mapper(User, users) + sess = fixture_session() + + result = ( + sess.query(Address) + .options(immediateload(Address.user)) + .filter(Address.id == 1) + .all() + ) + eq_(len(sess.identity_map), 2) + + sess.close() + + eq_( + [Address(id=1, email_address="jack@bean.com", user=User(id=7))], + result, + ) + def test_basic(self): Address, addresses, users, User = ( self.classes.Address, diff --git a/test/orm/test_selectin_relations.py b/test/orm/test_selectin_relations.py index 21d5e827d9..ec642a71ce 100644 --- a/test/orm/test_selectin_relations.py +++ b/test/orm/test_selectin_relations.py @@ -1459,12 +1459,12 @@ class LoadOnExistingTest(_fixtures.FixtureTest): sess = fixture_session(autoflush=False) return User, Order, Item, sess - def _eager_config_fixture(self): + def _eager_config_fixture(self, default_lazy="selectin"): User, Address = self.classes.User, self.classes.Address mapper( User, self.tables.users, - properties={"addresses": relationship(Address, lazy="selectin")}, + properties={"addresses": relationship(Address, lazy=default_lazy)}, ) mapper(Address, self.tables.addresses) sess = fixture_session(autoflush=False) @@ -1497,6 +1497,32 @@ class LoadOnExistingTest(_fixtures.FixtureTest): self.assert_sql_count(testing.db, go, 2) assert "addresses" in u1.__dict__ + @testing.combinations( + ("raise",), + ("raise_on_sql",), + ("select",), + ("immediate"), + ) + def test_runs_query_on_option_refresh(self, default_lazy): + User, Address, sess = self._eager_config_fixture( + default_lazy=default_lazy + ) + + u1 = ( + sess.query(User) + .options(selectinload(User.addresses)) + .filter_by(id=8) + .first() + ) + assert "addresses" in u1.__dict__ + sess.expire(u1) + + def go(): + eq_(u1.id, 8) + + self.assert_sql_count(testing.db, go, 2) + assert "addresses" in u1.__dict__ + def test_no_query_on_deferred(self): User, Address, sess = self._deferred_config_fixture() u1 = sess.query(User).get(8) diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py index c12b6fad3c..5fa9254c02 100644 --- a/test/orm/test_subquery_relations.py +++ b/test/orm/test_subquery_relations.py @@ -1489,12 +1489,12 @@ class LoadOnExistingTest(_fixtures.FixtureTest): sess = fixture_session(autoflush=False) return User, Order, Item, sess - def _eager_config_fixture(self): + def _eager_config_fixture(self, default_lazy="subquery"): User, Address = self.classes.User, self.classes.Address mapper( User, self.tables.users, - properties={"addresses": relationship(Address, lazy="subquery")}, + properties={"addresses": relationship(Address, lazy=default_lazy)}, ) mapper(Address, self.tables.addresses) sess = fixture_session(autoflush=False) @@ -1527,6 +1527,32 @@ class LoadOnExistingTest(_fixtures.FixtureTest): self.assert_sql_count(testing.db, go, 2) assert "addresses" in u1.__dict__ + @testing.combinations( + ("raise",), + ("raise_on_sql",), + ("select",), + ("immediate"), + ) + def test_runs_query_on_option_refresh(self, default_lazy): + User, Address, sess = self._eager_config_fixture( + default_lazy=default_lazy + ) + + u1 = ( + sess.query(User) + .options(subqueryload(User.addresses)) + .filter_by(id=8) + .first() + ) + assert "addresses" in u1.__dict__ + sess.expire(u1) + + def go(): + eq_(u1.id, 8) + + self.assert_sql_count(testing.db, go, 2) + assert "addresses" in u1.__dict__ + def test_no_query_on_deferred(self): User, Address, sess = self._deferred_config_fixture() u1 = sess.query(User).get(8)