From: Mike Bayer Date: Tue, 20 Apr 2021 18:09:51 +0000 (-0400) Subject: Allow immediateload to use_get for recursive call X-Git-Tag: rel_1_4_10~3^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=4b9e2db51a7e40ff2c4212fe8476fe0c7ba21ca0;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Allow immediateload to use_get for recursive call 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 --- diff --git a/doc/build/changelog/unreleased_14/6301.rst b/doc/build/changelog/unreleased_14/6301.rst new file mode 100644 index 0000000000..a85ffd8f89 --- /dev/null +++ b/doc/build/changelog/unreleased_14/6301.rst @@ -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. + diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index ea6d0f1fe6..cd2ec8301f 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -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) diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 4936049d4f..0f68a3fef8 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -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)) diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index 2cab0d0f0f..8602f37b60 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -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. diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py index 421568b65a..de49e3d18f 100644 --- a/test/orm/test_eager_relations.py +++ b/test/orm/test_eager_relations.py @@ -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 diff --git a/test/orm/test_immediate_load.py b/test/orm/test_immediate_load.py index 0e166950e6..1164b7ffdd 100644 --- a/test/orm/test_immediate_load.py +++ b/test/orm/test_immediate_load.py @@ -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__