]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Allow immediateload to use_get for recursive call
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 20 Apr 2021 18:09:51 +0000 (14:09 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 20 Apr 2021 18:11:44 +0000 (14:11 -0400)
Altered some of the behavior repaired in :ticket:`6232` where the
``immediateload`` loader strategy no longer goes into recursive loops; the
modification is that an eager load (joinedload, selectinload, or
subqueryload) from A->bs->B which then states ``immediateload`` for a
simple manytoone B->a->A that's in the identity map will populate the B->A,
so that this attribute is back-populated when the collection of A/A.bs are
loaded. This allows the objects to be functional when detached.

Fixes: #6301
Change-Id: I8505d851802c38ad8ad4e2fab9030f7c17089e9d

doc/build/changelog/unreleased_14/6301.rst [new file with mode: 0644]
lib/sqlalchemy/orm/loading.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/strategy_options.py
test/orm/test_eager_relations.py
test/orm/test_immediate_load.py

diff --git a/doc/build/changelog/unreleased_14/6301.rst b/doc/build/changelog/unreleased_14/6301.rst
new file mode 100644 (file)
index 0000000..a85ffd8
--- /dev/null
@@ -0,0 +1,12 @@
+.. change::
+    :tags: usecase, orm
+    :ticketS: 6301
+
+    Altered some of the behavior repaired in :ticket:`6232` where the
+    ``immediateload`` loader strategy no longer goes into recursive loops; the
+    modification is that an eager load (joinedload, selectinload, or
+    subqueryload) from A->bs->B which then states ``immediateload`` for a
+    simple manytoone B->a->A that's in the identity map will populate the B->A,
+    so that this attribute is back-populated when the collection of A/A.bs are
+    loaded. This allows the objects to be functional when detached.
+
index ea6d0f1fe673e81c42fae38a5b1dc18f088d4520..cd2ec8301f6654f6f2e654e3a1c8950547e543b0 100644 (file)
@@ -338,7 +338,9 @@ def get_from_identity(session, mapper, key, passive):
                 return attributes.PASSIVE_NO_RESULT
             elif not passive & attributes.RELATED_OBJECT_OK:
                 # this mode is used within a flush and the instance's
-                # expired state will be checked soon enough, if necessary
+                # expired state will be checked soon enough, if necessary.
+                # also used by immediateloader for a mutually-dependent
+                # o2m->m2m load, :ticket:`6301`
                 return instance
             try:
                 state._load_expired(state, passive)
index 4936049d4f9704c41290200eee3f9e39872145fb..0f68a3fef8f7826dac5df47c5f34fef09354752d 100644 (file)
@@ -1237,12 +1237,16 @@ class ImmediateLoader(PostLoader):
         populators,
     ):
         def load_immediate(state, dict_, row):
-            state.get_impl(self.key).get(
-                state, dict_, attributes.PASSIVE_OFF | attributes.NO_RAISE
-            )
+            state.get_impl(self.key).get(state, dict_, flags)
 
         if self._check_recursive_postload(context, path):
-            return
+            # this will not emit SQL and will only emit for a many-to-one
+            # "use get" load.   the "_RELATED" part means it may return
+            # instance even if its expired, since this is a mutually-recursive
+            # load operation.
+            flags = attributes.PASSIVE_NO_FETCH_RELATED | attributes.NO_RAISE
+        else:
+            flags = attributes.PASSIVE_OFF | attributes.NO_RAISE
 
         populators["delayed"].append((self.key, load_immediate))
 
index 2cab0d0f0fa0000627d05b9421e14a0c2e5b427b..8602f37b60c4ee057664d8846e9b8d397202d7f9 100644 (file)
@@ -1380,6 +1380,9 @@ def immediateload(loadopt, attr):
     """Indicate that the given attribute should be loaded using
     an immediate load with a per-attribute SELECT statement.
 
+    The load is achieved using the "lazyloader" strategy and does not
+    fire off any additional eager loaders.
+
     The :func:`.immediateload` option is superseded in general
     by the :func:`.selectinload` option, which performs the same task
     more efficiently by emitting a SELECT for all loaded objects.
index 421568b65aaae6f617aee826a52de9609c5548dd..de49e3d18f3eb3b2f568c8759c00d9e86d39b209 100644 (file)
@@ -3741,6 +3741,9 @@ class LoadOnExistingTest(_fixtures.FixtureTest):
 
         assert "addresses" in u1.__dict__
 
+        # immediateload would be used here for all 3 strategies
+        assert "user" in u1.addresses[0].__dict__
+
     def test_populate_existing_propagate(self):
         # both SelectInLoader and SubqueryLoader receive the loaded collection
         # at once and use attributes.set_committed_value().  However
index 0e166950e6bf53e2c84217006603653ae62d2931..1164b7ffddd5864d4595f29c8fc1d30acd27c3d3 100644 (file)
@@ -121,3 +121,84 @@ class ImmediateTest(_fixtures.FixtureTest):
             ],
             result,
         )
+
+    @testing.combinations(
+        ("joined",),
+        ("selectin",),
+        ("subquery",),
+    )
+    def test_m2one_side(self, o2m_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="immediate", back_populates="addresses"
+                )
+            },
+        )
+        mapper(
+            User,
+            users,
+            properties={
+                "addresses": relationship(
+                    Address, lazy=o2m_lazy, back_populates="user"
+                )
+            },
+        )
+        sess = fixture_session()
+        u1 = sess.query(User).filter(users.c.id == 7).one()
+        sess.close()
+
+        assert "addresses" in u1.__dict__
+        assert "user" in u1.addresses[0].__dict__
+
+    @testing.combinations(
+        ("immediate",),
+        ("joined",),
+        ("selectin",),
+        ("subquery",),
+    )
+    def test_o2mone_side(self, m2o_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=m2o_lazy, back_populates="addresses"
+                )
+            },
+        )
+        mapper(
+            User,
+            users,
+            properties={
+                "addresses": relationship(
+                    Address, lazy="immediate", back_populates="user"
+                )
+            },
+        )
+        sess = fixture_session()
+        u1 = sess.query(User).filter(users.c.id == 7).one()
+        sess.close()
+
+        assert "addresses" in u1.__dict__
+
+        # current behavior of "immediate" is that subsequent eager loaders
+        # aren't fired off.  This is because the "lazyload" strategy
+        # does not invoke eager loaders.
+        assert "user" not in u1.addresses[0].__dict__