]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- An adjustment to the :func:`.subqueryload` strategy which ensures that
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 17 Dec 2013 00:17:41 +0000 (19:17 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 17 Dec 2013 00:22:26 +0000 (19:22 -0500)
the query runs after the loading process has begun; this is so that
the subqueryload takes precedence over other loaders that may be
hitting the same attribute due to other eager/noload situations
at the wrong time. [ticket:2887]

doc/build/changelog/changelog_08.rst
lib/sqlalchemy/orm/strategies.py
test/orm/test_subquery_relations.py

index 3b30333f2821ba3e23eecf0e709ba41a65e65b93..9e8d2af1d6ade1a92f320d795f9b290ee1c69289 100644 (file)
 .. changelog::
     :version: 0.8.5
 
+    .. change::
+        :tags: bug, orm
+        :versions: 0.9.0b2
+        :tickets: 2887
+
+        An adjustment to the :func:`.subqueryload` strategy which ensures that
+        the query runs after the loading process has begun; this is so that
+        the subqueryload takes precedence over other loaders that may be
+        hitting the same attribute due to other eager/noload situations
+        at the wrong time.
+
     .. change::
         :tags: bug, orm
         :versions: 0.9.0b2
index b8ab55da4f45d79c5a5913030ea4cb8f103511e6..8b4c4f098effe122cf9a89df3266c5cf5cf357f4 100644 (file)
@@ -931,6 +931,35 @@ class SubqueryLoader(AbstractRelationshipLoader):
             q = q.order_by(*eager_order_by)
         return q
 
+    class _SubqCollections(object):
+        """Given a :class:`.Query` used to emit the "subquery load",
+        provide a load interface that executes the query at the
+        first moment a value is needed.
+
+        """
+        _data = None
+
+        def __init__(self, subq):
+            self.subq = subq
+
+        def get(self, key, default):
+            if self._data is None:
+                self._load()
+            return self._data.get(key, default)
+
+        def _load(self):
+            self._data = dict(
+                (k, [vv[0] for vv in v])
+                for k, v in itertools.groupby(
+                    self.subq,
+                    lambda x: x[1:]
+                )
+            )
+
+        def loader(self, state, dict_, row):
+            if self._data is None:
+                self._load()
+
     def create_row_processor(self, context, path,
                                     mapper, row, adapter):
         if not self.parent.class_manager[self.key].impl.supports_population:
@@ -953,12 +982,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
         # call upon create_row_processor again
         collections = path.get(context, "collections")
         if collections is None:
-            collections = dict(
-                    (k, [v[0] for v in v])
-                    for k, v in itertools.groupby(
-                        subq,
-                        lambda x: x[1:]
-                    ))
+            collections = self._SubqCollections(subq)
             path.set(context, 'collections', collections)
 
         if adapter:
@@ -978,7 +1002,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
             state.get_impl(self.key).\
                     set_committed_value(state, dict_, collection)
 
-        return load_collection_from_subq, None, None
+        return load_collection_from_subq, None, None, collections.loader
 
     def _create_scalar_loader(self, collections, local_cols):
         def load_scalar_from_subq(state, dict_, row):
@@ -996,7 +1020,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
             state.get_impl(self.key).\
                     set_committed_value(state, dict_, scalar)
 
-        return load_scalar_from_subq, None, None
+        return load_scalar_from_subq, None, None, collections.loader
 
 
 log.class_logger(SubqueryLoader)
index cad29ebfa76fd80c00a674b4307c39903e502a7f..26feff689e4b256991ea9ccbe953f4733122f4cf 100644 (file)
@@ -10,6 +10,7 @@ from sqlalchemy.testing import eq_, assert_raises, \
     assert_raises_message
 from sqlalchemy.testing.assertsql import CompiledSQL
 from sqlalchemy.testing import fixtures
+from sqlalchemy.testing.entities import ComparableEntity
 from test.orm import _fixtures
 import sqlalchemy as sa
 
@@ -1764,3 +1765,56 @@ class SubqueryloadDistinctTest(fixtures.DeclarativeMappedTest,
                     (1, 'Woody Allen', 1)
             ]
         )
+
+
+class JoinedNoLoadConflictTest(fixtures.DeclarativeMappedTest):
+    """test for [ticket:2887]"""
+
+    @classmethod
+    def setup_classes(cls):
+        Base = cls.DeclarativeBasic
+
+        class Parent(ComparableEntity, Base):
+            __tablename__ = 'parent'
+
+            id = Column(Integer, primary_key=True)
+            name = Column(String(20))
+
+            children = relationship('Child',
+                                back_populates='parent',
+                                lazy='noload'
+                            )
+
+        class Child(ComparableEntity, Base):
+            __tablename__ = 'child'
+
+            id = Column(Integer, primary_key=True)
+            name = Column(String(20))
+            parent_id = Column(Integer, ForeignKey('parent.id'))
+
+            parent = relationship('Parent', back_populates='children', lazy='joined')
+
+    @classmethod
+    def insert_data(cls):
+        Parent = cls.classes.Parent
+        Child = cls.classes.Child
+
+        s = Session()
+        s.add(Parent(name='parent', children=[Child(name='c1')]))
+        s.commit()
+
+    def test_subqueryload_on_joined_noload(self):
+        Parent = self.classes.Parent
+        Child = self.classes.Child
+
+        s = Session()
+
+        # here we have Parent->subqueryload->Child->joinedload->parent->noload->children.
+        # the actual subqueryload has to emit *after* we've started populating
+        # Parent->subqueryload->child.
+        parent = s.query(Parent).options([subqueryload('children')]).first()
+        eq_(
+            parent.children,
+            [Child(name='c1')]
+        )
+