From: Mike Bayer Date: Tue, 7 Jan 2020 02:06:10 +0000 (-0500) Subject: Set use_mapper_path=True for with_poly subentities X-Git-Tag: rel_1_3_13~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0b9283149d57ac44e7efe63fe06e0eebd57c0425;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Set use_mapper_path=True for with_poly subentities Fixed regression in joined eager loading introduced in 1.3.0b3 via :ticket:`4468` where the ability to create a joined option across a :func:`.with_polymorphic` into a polymorphic subclass using :meth:`.RelationshipProperty.of_type` and then further along regular mapped relationships would fail as the polymorphic subclass would not add itself to the load path in a way that could be located by the loader strategy. A tweak has been made to resolve this scenario. Fixes: #5082 Change-Id: I1c7b8d70ed94436c655e433bf34394b13d384c35 (cherry picked from commit 2734439fff953a7bb8aecdedb5f851441b5122e9) --- diff --git a/doc/build/changelog/unreleased_13/5082.rst b/doc/build/changelog/unreleased_13/5082.rst new file mode 100644 index 0000000000..000f55a192 --- /dev/null +++ b/doc/build/changelog/unreleased_13/5082.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: orm, bug + :tickets: 5082 + + Fixed regression in joined eager loading introduced in 1.3.0b3 via + :ticket:`4468` where the ability to create a joined option across a + :func:`.with_polymorphic` into a polymorphic subclass using + :meth:`.RelationshipProperty.of_type` and then further along regular mapped + relationships would fail as the polymorphic subclass would not add itself + to the load path in a way that could be located by the loader strategy. A + tweak has been made to resolve this scenario. + diff --git a/lib/sqlalchemy/orm/path_registry.py b/lib/sqlalchemy/orm/path_registry.py index 300fdafee0..1215511a1d 100644 --- a/lib/sqlalchemy/orm/path_registry.py +++ b/lib/sqlalchemy/orm/path_registry.py @@ -247,7 +247,10 @@ class PropRegistry(PathRegistry): and prop.parent in insp.with_polymorphic_mappers ): subclass_entity = parent[-1]._entity_for_mapper(prop.parent) - parent = parent.parent[subclass_entity] + if subclass_entity._use_mapper_path: + parent = parent.parent[subclass_entity.mapper] + else: + parent = parent.parent[subclass_entity] self.prop = prop self.parent = parent diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index f898b546c9..d9c12ac21e 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -287,6 +287,7 @@ class Load(Generative, MapperOption): existing = path.entity_path[prop].get( self.context, "path_with_polymorphic" ) + if not ext_info.is_aliased_class: ac = orm_util.with_polymorphic( ext_info.mapper.base_mapper, diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 08af18d18b..ed5d113c8f 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -603,7 +603,7 @@ class AliasedInsp(InspectionAttr): selectable, base_alias=self, adapt_on_names=adapt_on_names, - use_mapper_path=_use_mapper_path, + use_mapper_path=True, ) setattr(self.entity, poly.class_.__name__, ent) diff --git a/test/orm/inheritance/test_relationship.py b/test/orm/inheritance/test_relationship.py index 3c121e06be..da7136fc98 100644 --- a/test/orm/inheritance/test_relationship.py +++ b/test/orm/inheritance/test_relationship.py @@ -1725,6 +1725,106 @@ class SubClassToSubClassMultiTest(AssertsCompiledSQL, fixtures.MappedTest): ) +class JoinedloadWPolyOfTypeContinued( + fixtures.DeclarativeMappedTest, testing.AssertsCompiledSQL +): + """test for #5082 """ + + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic + + class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True) + + class Foo(Base): + __tablename__ = "foos" + __mapper_args__ = {"polymorphic_on": "type"} + + id = Column(Integer, primary_key=True) + type = Column(String(10), nullable=False) + owner_id = Column(Integer, ForeignKey("users.id")) + owner = relationship( + "User", backref=backref("foos"), cascade="all" + ) + + class SubFoo(Foo): + __tablename__ = "foos_sub" + __mapper_args__ = {"polymorphic_identity": "SUB"} + + id = Column(Integer, ForeignKey("foos.id"), primary_key=True) + baz = Column(Integer) + bar_id = Column(Integer, ForeignKey("bars.id")) + bar = relationship("Bar") + + class Bar(Base): + __tablename__ = "bars" + + id = Column(Integer, primary_key=True) + fred_id = Column(Integer, ForeignKey("freds.id"), nullable=False) + fred = relationship("Fred") + + class Fred(Base): + __tablename__ = "freds" + + id = Column(Integer, primary_key=True) + + @classmethod + def insert_data(cls): + User, Fred, Bar, SubFoo = cls.classes("User", "Fred", "Bar", "SubFoo") + user = User(id=1) + fred = Fred(id=1) + bar = Bar(fred=fred) + rectangle = SubFoo(owner=user, baz=10, bar=bar) + + s = Session() + s.add_all([user, fred, bar, rectangle]) + s.commit() + + def test_joined_load(self): + Foo, User, Bar = self.classes("Foo", "User", "Bar") + + s = Session() + + foo_polymorphic = with_polymorphic(Foo, "*", aliased=True) + + foo_load = joinedload(User.foos.of_type(foo_polymorphic)) + query = s.query(User).options( + foo_load.joinedload(foo_polymorphic.SubFoo.bar).joinedload( + Bar.fred + ) + ) + + self.assert_compile( + query, + "SELECT users.id AS users_id, anon_1.foos_id AS anon_1_foos_id, " + "anon_1.foos_type AS anon_1_foos_type, anon_1.foos_owner_id " + "AS anon_1_foos_owner_id, freds_1.id AS freds_1_id, bars_1.id " + "AS bars_1_id, bars_1.fred_id AS bars_1_fred_id, " + "anon_1.foos_sub_id AS anon_1_foos_sub_id, " + "anon_1.foos_sub_baz AS anon_1_foos_sub_baz, " + "anon_1.foos_sub_bar_id AS anon_1_foos_sub_bar_id " + "FROM users LEFT OUTER JOIN " + "(SELECT foos.id AS foos_id, foos.type AS foos_type, " + "foos.owner_id AS foos_owner_id, foos_sub.id AS foos_sub_id, " + "foos_sub.baz AS foos_sub_baz, foos_sub.bar_id AS foos_sub_bar_id " + "FROM foos LEFT OUTER JOIN foos_sub ON foos.id = foos_sub.id) " + "AS anon_1 ON users.id = anon_1.foos_owner_id " + "LEFT OUTER JOIN bars AS bars_1 " + "ON bars_1.id = anon_1.foos_sub_bar_id " + "LEFT OUTER JOIN freds AS freds_1 ON freds_1.id = bars_1.fred_id", + ) + + def go(): + user = query.one() + user.foos[0].bar + user.foos[0].bar.fred + + self.assert_sql_count(testing.db, go, 1) + + class JoinedloadSinglePolysubSingle( fixtures.DeclarativeMappedTest, testing.AssertsCompiledSQL ): diff --git a/test/orm/test_utils.py b/test/orm/test_utils.py index b82e2e3f76..2b4eadaf0e 100644 --- a/test/orm/test_utils.py +++ b/test/orm/test_utils.py @@ -918,14 +918,19 @@ class PathRegistryInhTest(_poly_fixtures._Polymorphic): emapper = inspect(Engineer) p_poly = with_polymorphic(Person, [Engineer]) - e_poly = inspect(p_poly.Engineer) + e_poly = inspect(p_poly.Engineer) # noqa - used by comment below p_poly_insp = inspect(p_poly) p1 = PathRegistry.coerce((p_poly_insp, emapper.attrs.machines)) - # polymorphic AliasedClass - the path uses _entity_for_mapper() - # to get the most specific sub-entity - eq_(p1.path, (e_poly, emapper.attrs.machines)) + # polymorphic AliasedClass - as of #5082, for the sub entities that are + # generated for each subclass by with_polymorphic(), use_mapper_path + # is not True so that creating paths from the sub entities, which don't + # by themselves encapsulate the with_polymorphic selectable, states the + # path in terms of that plain entity. previously, this path would be + # (e_poly, emapper.attrs.machines), but a loader strategy would never + # match on "e_poly", it would see "emapper". + eq_(p1.path, (emapper, emapper.attrs.machines)) def test_with_poly_base(self): Person = _poly_fixtures.Person