]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
use load_scalar_attributes() for undefer
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 15 Dec 2021 20:00:28 +0000 (15:00 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 19 Dec 2021 18:49:38 +0000 (13:49 -0500)
At some point, undeferral of attributes started emitting
a full ORM query for the entity, including that subclass columns
would use a JOIN.   Seems to be at least in 1.3 / 1.4 and possibly
earlier.    This JOINs to the superclass table unnecessarily.
Use load_scalar_attributes() here which should handle the whole
thing and emits a more efficient query for joined inheritance.

As this behavior seems to have been throughout 1.3 and 1.4
at least, targeting at 2.0 is likely best.

Fixes: #7463
Change-Id: Ie4bae767747bba0d03fb13eaff579d4bab0b1bc2

doc/build/changelog/unreleased_20/7463.rst [new file with mode: 0644]
lib/sqlalchemy/orm/loading.py
lib/sqlalchemy/orm/strategies.py
test/orm/inheritance/test_basic.py
test/orm/inheritance/test_poly_loading.py

diff --git a/doc/build/changelog/unreleased_20/7463.rst b/doc/build/changelog/unreleased_20/7463.rst
new file mode 100644 (file)
index 0000000..766dd53
--- /dev/null
@@ -0,0 +1,13 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 7463
+
+    Fixed performance regression which appeared at least in version 1.3 if not
+    earlier (sometime after 1.0) where the loading of deferred columns, those
+    explicitly mapped with :func:`_orm.defer` as opposed to non-deferred
+    columns that were expired, from a joined inheritance subclass would not use
+    the "optimized" query which only queried the immediate table that contains
+    the unloaded columns, instead running a full ORM query which would emit a
+    JOIN for all base tables, which is not necessary when only loading columns
+    from the subclass.
+
index d6ee9b7a72a23eb93e925669fc12052ec507aea3..a37d83cfeae35145d9afb75b4b399843fb46521d 100644 (file)
@@ -16,7 +16,6 @@ as well as some of the attribute loading strategies.
 from . import attributes
 from . import exc as orm_exc
 from . import path_registry
-from . import strategy_options
 from .base import _DEFER_FOR_STATE
 from .base import _RAISE_FOR_STATE
 from .base import _SET_DEFERRED_EXPIRED
@@ -1386,25 +1385,14 @@ def load_scalar_attributes(mapper, state, attribute_names, passive):
         attribute_names = attribute_names.intersection(mapper.attrs.keys())
 
     if mapper.inherits and not mapper.concrete:
-        # because we are using Core to produce a select() that we
-        # pass to the Query, we aren't calling setup() for mapped
-        # attributes; in 1.0 this means deferred attrs won't get loaded
-        # by default
         statement = mapper._optimized_get_statement(state, attribute_names)
         if statement is not None:
-            # this was previously aliased(mapper, statement), however,
-            # statement is a select() and Query's coercion now raises for this
-            # since you can't "select" from a "SELECT" statement.  only
-            # from_statement() allows this.
-            # note: using from_statement() here means there is an adaption
-            # with adapt_on_names set up.  the other option is to make the
-            # aliased() against a subquery which affects the SQL.
 
             from .query import FromStatement
 
-            stmt = FromStatement(mapper, statement).options(
-                strategy_options.Load(mapper).undefer("*")
-            )
+            # undefer() isn't needed here because statement has the
+            # columns needed already, this implicitly undefers that column
+            stmt = FromStatement(mapper, statement)
 
             result = load_on_ident(
                 session,
index ff4ac9d335c4fcde3e7f20f5018d2ad042d388f7..51e81e104d7c393977b5f5b5703400aca7293326 100644 (file)
@@ -24,6 +24,7 @@ from . import util as orm_util
 from .base import _DEFER_FOR_STATE
 from .base import _RAISE_FOR_STATE
 from .base import _SET_DEFERRED_EXPIRED
+from .base import PASSIVE_OFF
 from .context import _column_descriptions
 from .context import ORMCompileState
 from .context import QueryContext
@@ -512,19 +513,9 @@ class DeferredColumnLoader(LoaderStrategy):
         if self.raiseload:
             self._invoke_raise_load(state, passive, "raise")
 
-        if (
-            loading.load_on_ident(
-                session,
-                sql.select(localparent).set_label_style(
-                    LABEL_STYLE_TABLENAME_PLUS_COL
-                ),
-                state.key,
-                only_load_props=group,
-                refresh_state=state,
-            )
-            is None
-        ):
-            raise orm_exc.ObjectDeletedError(state)
+        loading.load_scalar_attributes(
+            state.mapper, state, set(group), PASSIVE_OFF
+        )
 
         return attributes.ATTR_WAS_SET
 
index 339a6b956696b5db6a48226bc54613d67363b77f..1dc01053b5f4fedc0e2abb4f304d4e8ad19e7d37 100644 (file)
@@ -1777,7 +1777,19 @@ class PassiveDeletesTest(fixtures.MappedTest):
 
 
 class OptimizedGetOnDeferredTest(fixtures.MappedTest):
-    """test that the 'optimized get' path accommodates deferred columns."""
+    """test that the 'optimized get' path accommodates deferred columns.
+
+    Original issue tested is #3468, where loading of a deferred column
+    in an inherited subclass would fail.
+
+    At some point, the logic tested was no longer used and a less efficient
+    query was used to load these columns, but the test here did not inspect
+    the SQL such that this would be detected.
+
+    Test was then revised to more carefully test and now targets
+    #7463 as well.
+
+    """
 
     @classmethod
     def define_tables(cls, metadata):
@@ -1787,6 +1799,7 @@ class OptimizedGetOnDeferredTest(fixtures.MappedTest):
             Column(
                 "id", Integer, primary_key=True, test_needs_autoincrement=True
             ),
+            Column("type", String(10)),
         )
         Table(
             "b",
@@ -1808,11 +1821,12 @@ class OptimizedGetOnDeferredTest(fixtures.MappedTest):
         A, B = cls.classes("A", "B")
         a, b = cls.tables("a", "b")
 
-        cls.mapper_registry.map_imperatively(A, a)
+        cls.mapper_registry.map_imperatively(A, a, polymorphic_on=a.c.type)
         cls.mapper_registry.map_imperatively(
             B,
             b,
             inherits=A,
+            polymorphic_identity="b",
             properties={
                 "data": deferred(b.c.data),
                 "expr": column_property(b.c.data + "q", deferred=True),
@@ -1825,8 +1839,17 @@ class OptimizedGetOnDeferredTest(fixtures.MappedTest):
         b1 = B(data="x")
         sess.add(b1)
         sess.flush()
+        b_id = b1.id
 
-        eq_(b1.expr, "xq")
+        with self.sql_execution_asserter(testing.db) as asserter:
+            eq_(b1.expr, "xq")
+        asserter.assert_(
+            CompiledSQL(
+                "SELECT b.data || :data_1 AS anon_1 "
+                "FROM b WHERE :param_1 = b.id",
+                [{"param_1": b_id, "data_1": "q"}],
+            )
+        )
 
     def test_expired_column(self):
         A, B = self.classes("A", "B")
@@ -1834,9 +1857,74 @@ class OptimizedGetOnDeferredTest(fixtures.MappedTest):
         b1 = B(data="x")
         sess.add(b1)
         sess.flush()
+        b_id = b1.id
         sess.expire(b1, ["data"])
 
+        with self.sql_execution_asserter(testing.db) as asserter:
+            eq_(b1.data, "x")
+        # uses efficient statement w/o JOIN to a
+        asserter.assert_(
+            CompiledSQL(
+                "SELECT b.data AS b_data FROM b WHERE :param_1 = b.id",
+                [{"param_1": b_id}],
+            )
+        )
+
+    def test_load_from_unloaded_subclass(self):
+        A, B = self.classes("A", "B")
+        sess = fixture_session()
+        b1 = B(data="x")
+        sess.add(b1)
+        sess.commit()
+        b_id = b1.id
+        sess.close()
+
+        # load polymorphically in terms of A, so that B needs another
+        # SELECT
+        b1 = sess.execute(select(A)).scalar()
+
+        # it's not loaded
+        assert "data" not in b1.__dict__
+
+        # but it loads successfully when requested
+        with self.sql_execution_asserter(testing.db) as asserter:
+            eq_(b1.data, "x")
+
+        # uses efficient statement w/o JOIN to a
+        asserter.assert_(
+            CompiledSQL(
+                "SELECT b.data AS b_data FROM b WHERE :param_1 = b.id",
+                [{"param_1": b_id}],
+            )
+        )
+
+    def test_load_from_expired_subclass(self):
+        A, B = self.classes("A", "B")
+        sess = fixture_session()
+        b1 = B(data="x")
+        sess.add(b1)
+        sess.commit()
+        b_id = b1.id
+        sess.close()
+
+        b1 = sess.execute(select(A)).scalar()
+
+        # it's not loaded
+        assert "data" not in b1.__dict__
+
         eq_(b1.data, "x")
+        sess.expire(b1, ["data"])
+
+        with self.sql_execution_asserter(testing.db) as asserter:
+            eq_(b1.data, "x")
+
+        # uses efficient statement w/o JOIN to a
+        asserter.assert_(
+            CompiledSQL(
+                "SELECT b.data AS b_data FROM b WHERE :param_1 = b.id",
+                [{"param_1": b_id}],
+            )
+        )
 
 
 class JoinedNoFKSortingTest(fixtures.MappedTest):
index fcaf470b45945c3e1009e00eac2a3ddd86fd94b4..0a03388314793ddf894b69a51c6f896c1e3fae8c 100644 (file)
@@ -846,10 +846,10 @@ class IgnoreOptionsOnSubclassAttrLoad(fixtures.DeclarativeMappedTest):
             # call to the deferred load put a deferred loader on the attribute
             expected.append(
                 CompiledSQL(
-                    "SELECT sub_entity.name AS sub_entity_name FROM entity "
-                    "JOIN sub_entity ON entity.id = sub_entity.id "
-                    "WHERE entity.id = :pk_1",
-                    [{"pk_1": entity_id}],
+                    "SELECT sub_entity.name AS sub_entity_name "
+                    "FROM sub_entity "
+                    "WHERE :param_1 = sub_entity.id",
+                    [{"param_1": entity_id}],
                 )
             )