]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Disable raiseload in immediateload strategy
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 13 Apr 2021 03:48:02 +0000 (23:48 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 13 Apr 2021 13:15:09 +0000 (09:15 -0400)
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

doc/build/changelog/unreleased_14/6252.rst [new file with mode: 0644]
doc/build/orm/queryguide.rst
doc/build/orm/session_state_management.rst
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/strategies.py
test/orm/test_immediate_load.py
test/orm/test_selectin_relations.py
test/orm/test_subquery_relations.py

diff --git a/doc/build/changelog/unreleased_14/6252.rst b/doc/build/changelog/unreleased_14/6252.rst
new file mode 100644 (file)
index 0000000..6a08754
--- /dev/null
@@ -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.
index 71893b81e8ce642f25525a46257c0741defdb9ff..58c278a1d3ba5afe74ce13a3e1db35d2314bcd24 100644 (file)
@@ -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:
index b91ffadb40b011251ca0e7b7a1a7e3210f28672c..d521d19e0e26a8b2288c755d065032cfe822cd4b 100644 (file)
@@ -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.
 
index d08abb78801713422086f4e7f8abcb1e625f4dc1..d5f418b1cada348a953f028b7a438fa4fecaa343 100644 (file)
@@ -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 <orm_queryguide_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:
index da8e0439feff3c29c1cceee30b5fb138e4cdfdec..efaf77a4f1b343b19ad44a0000ab44d2e4b144fd 100644 (file)
@@ -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
index 7efd3436c1197cd23de1d9562a1e1f03a6414246..0e166950e6bf53e2c84217006603653ae62d2931 100644 (file)
@@ -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,
index 21d5e827d97ab00f5f57de6be84c061f3a0fad6c..ec642a71ce5dc32ca80b5927dc19bff184a59642 100644 (file)
@@ -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)
index c12b6fad3c6481a0018e416b0898db0614e55309..5fa9254c027427e94ee018e19669e13bcf768284 100644 (file)
@@ -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)