diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-12-16 19:17:41 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-12-16 19:17:41 -0500 |
commit | 84f1d3417978197c695850b3711ea4b7e2582be8 (patch) | |
tree | a05d96f2198fb4b9fba2b3d7e888002f1d47218b | |
parent | 98c23acee6a81e55445c269193411d99f4b8c36a (diff) | |
download | sqlalchemy-84f1d3417978197c695850b3711ea4b7e2582be8.tar.gz |
- 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. [ticket:2887]
-rw-r--r-- | doc/build/changelog/changelog_08.rst | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 40 | ||||
-rw-r--r-- | test/orm/test_subquery_relations.py | 56 |
3 files changed, 98 insertions, 9 deletions
diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst index 70999ed6a..f137aebb7 100644 --- a/doc/build/changelog/changelog_08.rst +++ b/doc/build/changelog/changelog_08.rst @@ -14,6 +14,17 @@ .. 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 :tickets: 2885 Fixed bug when using joined table inheritance from a table to a diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 8226a0e0f..71bbf2bce 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -915,6 +915,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, loadopt, mapper, row, adapter): if not self.parent.class_manager[self.key].impl.supports_population: @@ -937,12 +966,7 @@ class SubqueryLoader(AbstractRelationshipLoader): # call upon create_row_processor again collections = path.get(context.attributes, "collections") if collections is None: - collections = dict( - (k, [vv[0] for vv in v]) - for k, v in itertools.groupby( - subq, - lambda x: x[1:] - )) + collections = self._SubqCollections(subq) path.set(context.attributes, 'collections', collections) if adapter: @@ -962,7 +986,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): @@ -980,7 +1004,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 diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py index 3181e0909..2740b5cf0 100644 --- a/test/orm/test_subquery_relations.py +++ b/test/orm/test_subquery_relations.py @@ -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 @@ -1753,4 +1754,57 @@ class SubqueryloadDistinctTest(fixtures.DeclarativeMappedTest, [ (1, 'Woody Allen', 1), (1, 'Woody Allen', 1), ] - )
\ No newline at end of file + ) + + +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')] + ) + |