From: Mike Bayer Date: Sat, 5 Feb 2011 21:09:49 +0000 (-0500) Subject: - A single contains_eager() call across X-Git-Tag: rel_0_7b1~35 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=85084caeffb458ac884c2140380cb8908965120b;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - 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. --- diff --git a/CHANGES b/CHANGES index a848035efd..2b1b3fbf11 100644 --- 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] diff --git a/doc/build/orm/loading.rst b/doc/build/orm/loading.rst index b62fac4884..8b79577ace 100644 --- a/doc/build/orm/loading.rst +++ b/doc/build/orm/loading.rst @@ -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 diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index b8b995f812..2ed270cda8 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -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 diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 8cece65cce..f68d2a289d 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -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))] = \ diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 333650ec4d..bf7f04995e 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -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", diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 6bcd73e505..a1a50f2ad1 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -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 diff --git a/test/orm/test_froms.py b/test/orm/test_froms.py index f07ddc7def..707f11426e 100644 --- a/test/orm/test_froms.py +++ b/test/orm/test_froms.py @@ -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']) diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 4de669b879..0ee5e44bd6 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -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)