]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- A single contains_eager() call across
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 5 Feb 2011 21:09:49 +0000 (16:09 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 5 Feb 2011 21:09:49 +0000 (16:09 -0500)
multiple entities will indicate all collections
along that path should load, instead of requiring
distinct contains_eager() calls for each endpoint
(which was never correctly documented).
[ticket:2032]

- The "name" field used in orm.aliased() now renders
in the resulting SQL statement.

CHANGES
doc/build/orm/loading.rst
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/util.py
test/orm/test_froms.py
test/orm/test_mapper.py

diff --git a/CHANGES b/CHANGES
index a848035efd043c7958dcaed7d5f70a9d139ffe1c..2b1b3fbf1156ea62f8499c456483d16daa691e28 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -91,6 +91,16 @@ CHANGES
     context.attributes where it's accessible by the 
     "load()" event. [ticket:2031]
 
+  - A single contains_eager() call across
+    multiple entities will indicate all collections
+    along that path should load, instead of requiring
+    distinct contains_eager() calls for each endpoint
+    (which was never correctly documented).
+    [ticket:2032]
+
+  - The "name" field used in orm.aliased() now renders
+    in the resulting SQL statement.
+
 - sql
   - LIMIT/OFFSET clauses now use bind parameters
     [ticket:805]
index b62fac48844378581f27911169aa157dc2f8a087..8b79577ace9bdb64b586bbd0f9f96eaeb1bf3cc7 100644 (file)
@@ -286,7 +286,7 @@ This is a string alias name or reference to an actual
 
     # construct a Query object which expects the "addresses" results
     query = session.query(User).\
-        outerjoin((adalias, User.addresses)).\
+        outerjoin(adalias, User.addresses).\
         options(contains_eager(User.addresses, alias=adalias))
 
     # get results normally
index b8b995f8121e4a0d4beae41d8720263707017cbb..2ed270cda839fe0f9ec489b5044faf85ab057dd6 100644 (file)
@@ -1196,7 +1196,8 @@ def noload(*keys):
 
     Used with :meth:`~sqlalchemy.orm.query.Query.options`.
 
-    See also:  :func:`lazyload`, :func:`eagerload`, :func:`subqueryload`, :func:`immediateload`
+    See also:  :func:`lazyload`, :func:`eagerload`, 
+    :func:`subqueryload`, :func:`immediateload`
 
     """
     return strategies.EagerLazyOption(keys, lazy=None)
@@ -1264,8 +1265,8 @@ def contains_eager(*keys, **kwargs):
         raise exceptions.ArgumentError('Invalid kwargs for contains_eag'
                 'er: %r' % kwargs.keys())
     return strategies.EagerLazyOption(keys, lazy='joined',
-            propagate_to_loaders=False), \
-        strategies.LoadEagerFromAliasOption(keys, alias=alias)
+            propagate_to_loaders=False, chained=True), \
+        strategies.LoadEagerFromAliasOption(keys, alias=alias, chained=True)
 
 def defer(*keys):
     """Return a ``MapperOption`` that will convert the column property of the
index 8cece65cce6cc1660ba2bad32ce2af74763ea27b..f68d2a289d8e726f61065dabd5aa6bbbe5e40ff6 100644 (file)
@@ -515,7 +515,7 @@ class StrategizedOption(PropertyOption):
     for an operation by a StrategizedProperty.
     """
 
-    is_chained = False
+    chained = False
 
     def process_query_property(self, query, paths, mappers):
 
@@ -525,7 +525,7 @@ class StrategizedOption(PropertyOption):
         # "(Person, 'machines')" in the path due to the mechanics of how
         # the eager strategy builds up the path
 
-        if self.is_chained:
+        if self.chained:
             for path in paths:
                 query._attributes[('loaderstrategy',
                                   _reduce_path(path))] = \
index 333650ec4d90248646d9b5f4c37674ed3a5a66df..bf7f04995e1aafa82224a7156fadce81dd727d07 100644 (file)
@@ -1195,18 +1195,10 @@ class EagerLazyOption(StrategizedOption):
                     ):
         super(EagerLazyOption, self).__init__(key)
         self.lazy = lazy
-        self.chained = chained
+        self.chained = self.lazy in (False, 'joined', 'subquery') and chained
         self.propagate_to_loaders = propagate_to_loaders
         self.strategy_cls = factory(lazy)
 
-    @property
-    def is_eager(self):
-        return self.lazy in (False, 'joined', 'subquery')
-
-    @property
-    def is_chained(self):
-        return self.is_eager and self.chained
-
     def get_strategy_class(self):
         return self.strategy_cls
 
@@ -1233,11 +1225,8 @@ class EagerJoinOption(PropertyOption):
         self.innerjoin = innerjoin
         self.chained = chained
 
-    def is_chained(self):
-        return self.chained
-
     def process_query_property(self, query, paths, mappers):
-        if self.is_chained():
+        if self.chained:
             for path in paths:
                 query._attributes[("eager_join_type", path)] = self.innerjoin
         else:
@@ -1245,19 +1234,28 @@ class EagerJoinOption(PropertyOption):
 
 class LoadEagerFromAliasOption(PropertyOption):
 
-    def __init__(self, key, alias=None):
+    def __init__(self, key, alias=None, chained=False):
         super(LoadEagerFromAliasOption, self).__init__(key)
         if alias is not None:
             if not isinstance(alias, basestring):
                 m, alias, is_aliased_class = mapperutil._entity_info(alias)
         self.alias = alias
+        self.chained = chained
 
     def process_query_property(self, query, paths, mappers):
+        if self.chained:
+            for path in paths[0:-1]:
+                (root_mapper, propname) = path[-2:]
+                prop = root_mapper._props[propname]
+                adapter = query._polymorphic_adapters.get(prop.mapper, None)
+                query._attributes.setdefault(
+                            ("user_defined_eager_row_processor", 
+                            interfaces._reduce_path(path)), adapter)
+
         if self.alias is not None:
             if isinstance(self.alias, basestring):
-                mapper = mappers[-1]
                 (root_mapper, propname) = paths[-1][-2:]
-                prop = mapper._props[propname]
+                prop = root_mapper._props[propname]
                 self.alias = prop.target.alias(self.alias)
             query._attributes[
                         ("user_defined_eager_row_processor", 
@@ -1265,8 +1263,7 @@ class LoadEagerFromAliasOption(PropertyOption):
                         ] = sql_util.ColumnAdapter(self.alias)
         else:
             (root_mapper, propname) = paths[-1][-2:]
-            mapper = mappers[-1]
-            prop = mapper._props[propname]
+            prop = root_mapper._props[propname]
             adapter = query._polymorphic_adapters.get(prop.mapper, None)
             query._attributes[
                         ("user_defined_eager_row_processor", 
index 6bcd73e505523c84e91472ecfc0aea1c44e1c7f1..a1a50f2ad18dd9629474ffbaa811912d23ca7f44 100644 (file)
@@ -212,7 +212,7 @@ class AliasedClass(object):
         self.__mapper = _class_to_mapper(cls)
         self.__target = self.__mapper.class_
         if alias is None:
-            alias = self.__mapper._with_polymorphic_selectable.alias()
+            alias = self.__mapper._with_polymorphic_selectable.alias(name=name)
         self.__adapter = sql_util.ClauseAdapter(alias,
                                 equivalents=self.__mapper._equivalent_columns)
         self.__alias = alias
index f07ddc7def71e31966ca1c290b7cc41f6c548686..707f11426e36537cc5326077b6100f78c51b26be 100644 (file)
@@ -190,7 +190,9 @@ class FromSelfTest(QueryTest, AssertsCompiledSQL):
         sess = create_session()
 
         eq_(
-            sess.query(User, Address).filter(User.id==Address.user_id).filter(Address.id.in_([2, 5])).from_self().all(),
+            sess.query(User, Address).\
+                    filter(User.id==Address.user_id).\
+                    filter(Address.id.in_([2, 5])).from_self().all(),
             [
                 (User(id=8), Address(id=2)),
                 (User(id=9), Address(id=5))
@@ -198,10 +200,15 @@ class FromSelfTest(QueryTest, AssertsCompiledSQL):
         )
 
         eq_(
-            sess.query(User, Address).filter(User.id==Address.user_id).filter(Address.id.in_([2, 5])).from_self().options(joinedload('addresses')).first(),
-
-            #    order_by(User.id, Address.id).first(),
-            (User(id=8, addresses=[Address(), Address(), Address()]), Address(id=2)),
+            sess.query(User, Address).\
+                    filter(User.id==Address.user_id).\
+                    filter(Address.id.in_([2, 5])).\
+                    from_self().\
+                    options(joinedload('addresses')).first(),
+
+            (User(id=8, 
+                    addresses=[Address(), Address(), Address()]), 
+                Address(id=2)),
         )
 
     def test_multiple_with_column_entities(self):
@@ -395,92 +402,214 @@ class InstancesTest(QueryTest, AssertsCompiledSQL):
         sess.expunge_all()
 
         adalias = addresses.alias()
-        q = sess.query(User).select_from(users.outerjoin(adalias)).options(contains_eager(User.addresses, alias=adalias)).order_by(User.id, adalias.c.id)
+        q = sess.query(User).\
+                select_from(users.outerjoin(adalias)).\
+                options(contains_eager(User.addresses, alias=adalias)).\
+                order_by(User.id, adalias.c.id)
         def go():
             eq_(self.static.user_address_result, q.order_by(User.id).all())
         self.assert_sql_count(testing.db, go, 1)
         sess.expunge_all()
 
-        selectquery = users.outerjoin(addresses).select(users.c.id<10, use_labels=True, order_by=[users.c.id, addresses.c.id])
+        selectquery = users.\
+                        outerjoin(addresses).\
+                        select(users.c.id<10, 
+                                use_labels=True, 
+                                order_by=[users.c.id, addresses.c.id])
         q = sess.query(User)
 
         def go():
-            l = list(q.options(contains_eager('addresses')).instances(selectquery.execute()))
+            l = list(q.options(
+                        contains_eager('addresses')
+                    ).instances(selectquery.execute()))
             assert self.static.user_address_result[0:3] == l
         self.assert_sql_count(testing.db, go, 1)
 
         sess.expunge_all()
 
         def go():
-            l = list(q.options(contains_eager(User.addresses)).instances(selectquery.execute()))
+            l = list(q.options(
+                        contains_eager(User.addresses)
+                        ).instances(selectquery.execute()))
             assert self.static.user_address_result[0:3] == l
         self.assert_sql_count(testing.db, go, 1)
         sess.expunge_all()
 
         def go():
-            l = q.options(contains_eager('addresses')).from_statement(selectquery).all()
+            l = q.options(
+                    contains_eager('addresses')
+                ).from_statement(selectquery).all()
             assert self.static.user_address_result[0:3] == l
         self.assert_sql_count(testing.db, go, 1)
 
-    def test_contains_eager_alias(self):
-        adalias = addresses.alias('adalias')
-        selectquery = users.outerjoin(adalias).select(use_labels=True, order_by=[users.c.id, adalias.c.id])
+    def test_contains_eager_string_alias(self):
         sess = create_session()
         q = sess.query(User)
 
+        adalias = addresses.alias('adalias')
+        selectquery = users.outerjoin(adalias).\
+                        select(use_labels=True, 
+                                order_by=[users.c.id, adalias.c.id])
+
         # string alias name
         def go():
-            l = list(q.options(contains_eager('addresses', alias="adalias")).instances(selectquery.execute()))
+            l = list(q.options(
+                            contains_eager('addresses', alias="adalias")
+                        ).instances(selectquery.execute()))
             assert self.static.user_address_result == l
         self.assert_sql_count(testing.db, go, 1)
-        sess.expunge_all()
+
+    def test_contains_eager_aliased_instances(self):
+        sess = create_session()
+        q = sess.query(User)
+
+        adalias = addresses.alias('adalias')
+        selectquery = users.outerjoin(adalias).\
+                        select(use_labels=True, 
+                            order_by=[users.c.id, adalias.c.id])
 
         # expression.Alias object
         def go():
-            l = list(q.options(contains_eager('addresses', alias=adalias)).instances(selectquery.execute()))
+            l = list(q.options(
+                        contains_eager('addresses', alias=adalias)
+                    ).instances(selectquery.execute()))
             assert self.static.user_address_result == l
         self.assert_sql_count(testing.db, go, 1)
 
-        sess.expunge_all()
+    def test_contains_eager_aliased(self):
+        sess = create_session()
+        q = sess.query(User)
 
         # Aliased object
         adalias = aliased(Address)
         def go():
-            l = q.options(contains_eager('addresses', alias=adalias)).outerjoin(adalias, User.addresses).order_by(User.id, adalias.id)
+            l = q.options(
+                    contains_eager('addresses', alias=adalias)
+                ).\
+                    outerjoin(adalias, User.addresses).\
+                    order_by(User.id, adalias.id)
             assert self.static.user_address_result == l.all()
         self.assert_sql_count(testing.db, go, 1)
-        sess.expunge_all()
+
+    def test_contains_eager_multi_string_alias(self):
+        sess = create_session()
+        q = sess.query(User)
 
         oalias = orders.alias('o1')
         ialias = items.alias('i1')
-        query = users.outerjoin(oalias).outerjoin(order_items).outerjoin(ialias).select(use_labels=True).order_by(users.c.id, oalias.c.id, ialias.c.id)
-        q = create_session().query(User)
+        query = users.outerjoin(oalias).\
+                    outerjoin(order_items).\
+                    outerjoin(ialias).\
+                    select(use_labels=True).\
+                    order_by(users.c.id, oalias.c.id, ialias.c.id)
+
         # test using string alias with more than one level deep
         def go():
-            l = list(q.options(contains_eager('orders', alias='o1'), contains_eager('orders.items', alias='i1')).instances(query.execute()))
+            l = list(q.options(
+                        contains_eager('orders', alias='o1'), 
+                        contains_eager('orders.items', alias='i1')
+                    ).instances(query.execute()))
             assert self.static.user_order_result == l
         self.assert_sql_count(testing.db, go, 1)
 
-        sess.expunge_all()
+    def test_contains_eager_multi_alias(self):
+        sess = create_session()
+        q = sess.query(User)
+
+        oalias = orders.alias('o1')
+        ialias = items.alias('i1')
+        query = users.outerjoin(oalias).\
+                    outerjoin(order_items).\
+                    outerjoin(ialias).\
+                    select(use_labels=True).\
+                    order_by(users.c.id, oalias.c.id, ialias.c.id)
 
         # test using Alias with more than one level deep
         def go():
-            l = list(q.options(contains_eager('orders', alias=oalias), contains_eager('orders.items', alias=ialias)).instances(query.execute()))
+            l = list(q.options(
+                    contains_eager('orders', alias=oalias), 
+                    contains_eager('orders.items', alias=ialias)
+                ).instances(query.execute()))
             assert self.static.user_order_result == l
         self.assert_sql_count(testing.db, go, 1)
-        sess.expunge_all()
+
+    def test_contains_eager_multi_aliased(self):
+        sess = create_session()
+        q = sess.query(User)
 
         # test using Aliased with more than one level deep
         oalias = aliased(Order)
         ialias = aliased(Item)
         def go():
-            l = q.options(contains_eager(User.orders, alias=oalias), 
-                            contains_eager(User.orders, Order.items, alias=ialias)).\
+            l = q.options(
+                    contains_eager(User.orders, alias=oalias), 
+                    contains_eager(User.orders, Order.items, alias=ialias)
+                ).\
                 outerjoin(oalias, User.orders).\
-                outerjoin(ialias, oalias.items).order_by(User.id, oalias.id, ialias.id)
+                outerjoin(ialias, oalias.items).\
+                order_by(User.id, oalias.id, ialias.id)
             assert self.static.user_order_result == l.all()
         self.assert_sql_count(testing.db, go, 1)
-        sess.expunge_all()
+
+    def test_contains_eager_chaining(self):
+        """test that contains_eager() 'chains' by default."""
+
+        sess = create_session()
+        q = sess.query(User).\
+                join(User.addresses).\
+                join(Address.dingaling).\
+                options(
+                    contains_eager(User.addresses, Address.dingaling), 
+                    )
+        def go():
+            eq_(
+                q.all(),
+                # note we only load the Address records that 
+                # have a Dingaling here due to using the inner 
+                # join for the eager load
+                [
+                    User(name=u'ed', addresses=[
+                        Address(email_address=u'ed@wood.com', 
+                                dingaling=Dingaling(data='ding 1/2')), 
+                    ]), 
+                    User(name=u'fred', addresses=[
+                        Address(email_address=u'fred@fred.com', 
+                                dingaling=Dingaling(data='ding 2/5'))
+                    ])
+                ]
+            )
+        self.assert_sql_count(testing.db, go, 1)
+
+    def test_contains_eager_chaining_aliased_endpoint(self):
+        """test that contains_eager() 'chains' by default and supports
+        an alias at the end."""
+
+        sess = create_session()
+        da = aliased(Dingaling, name="foob")
+        q = sess.query(User).\
+                join(User.addresses).\
+                join(da, Address.dingaling).\
+                options(
+                    contains_eager(User.addresses, Address.dingaling, alias=da), 
+                    )
+        def go():
+            eq_(
+                q.all(),
+                # note we only load the Address records that 
+                # have a Dingaling here due to using the inner 
+                # join for the eager load
+                [
+                    User(name=u'ed', addresses=[
+                        Address(email_address=u'ed@wood.com', 
+                                dingaling=Dingaling(data='ding 1/2')), 
+                    ]), 
+                    User(name=u'fred', addresses=[
+                        Address(email_address=u'fred@fred.com', 
+                                dingaling=Dingaling(data='ding 2/5'))
+                    ])
+                ]
+            )
+        self.assert_sql_count(testing.db, go, 1)
 
     def test_mixed_eager_contains_with_limit(self):
         sess = create_session()
@@ -591,6 +720,18 @@ class MixedEntitiesTest(QueryTest, AssertsCompiledSQL):
                 filter(users.c.id>sel.c.id).values(users.c.name, sel.c.name, User.name)
         eq_(list(q2), [(u'ed', u'jack', u'jack')])
 
+    def test_alias_naming(self):
+        sess = create_session()
+
+        ua = aliased(User, name="foobar")
+        q= sess.query(ua)
+        self.assert_compile(
+            q,
+            "SELECT foobar.id AS foobar_id, "
+            "foobar.name AS foobar_name FROM users AS foobar",
+            use_default_dialect=True
+        )
+
     @testing.fails_on('mssql', 'FIXME: unknown')
     def test_values_specific_order_by(self):
         sess = create_session()
@@ -701,7 +842,7 @@ class MixedEntitiesTest(QueryTest, AssertsCompiledSQL):
                 eq_(row.User, row[0])
 
             oalias = aliased(Order, name='orders')
-            for row in sess.query(User, oalias).join(User.orders).all():
+            for row in sess.query(User, oalias).join(oalias, User.orders).all():
                 if pickled is not False:
                     row = util.pickle.loads(util.pickle.dumps(row, pickled))
                 eq_(row.keys(), ['User', 'orders'])
index 4de669b879a460bd1baec411dd64f4e2b5c5ae93..0ee5e44bd6a8752ea29345d4c891642a7a73f7ff 100644 (file)
@@ -1979,8 +1979,12 @@ class SecondaryOptionsTest(_base.MappedTest):
         mapper(Base, base, polymorphic_on=base.c.type, properties={
             'related':relationship(Related, uselist=False)
         })
-        mapper(Child1, child1, inherits=Base, polymorphic_identity='child1', properties={
-            'child2':relationship(Child2, primaryjoin=child1.c.child2id==base.c.id, foreign_keys=child1.c.child2id)
+        mapper(Child1, child1, inherits=Base, 
+                polymorphic_identity='child1', 
+            properties={
+            'child2':relationship(Child2, 
+                                    primaryjoin=child1.c.child2id==base.c.id, 
+                                    foreign_keys=child1.c.child2id)
         })
         mapper(Child2, child2, inherits=Base, polymorphic_identity='child2')
         mapper(Related, related)
@@ -2019,13 +2023,19 @@ class SecondaryOptionsTest(_base.MappedTest):
     def test_contains_eager(self):
         sess = create_session()
 
-
-        child1s = sess.query(Child1).join(Child1.related).options(sa.orm.contains_eager(Child1.related)).order_by(Child1.id)
+        child1s = sess.query(Child1).\
+                    join(Child1.related).\
+                    options(sa.orm.contains_eager(Child1.related)).\
+                    order_by(Child1.id)
 
         def go():
             eq_(
                 child1s.all(),
-                [Child1(id=1, related=Related(id=1)), Child1(id=2, related=Related(id=2)), Child1(id=3, related=Related(id=3))]
+                [
+                    Child1(id=1, related=Related(id=1)), 
+                    Child1(id=2, related=Related(id=2)), 
+                    Child1(id=3, related=Related(id=3))
+                ]
             )
         self.assert_sql_count(testing.db, go, 1)