From: Mike Bayer Date: Tue, 26 Aug 2014 21:23:23 +0000 (-0400) Subject: - The behavior of :paramref:`.joinedload.innerjoin` as well as X-Git-Tag: rel_1_0_0b1~205^2~40 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=00862a29c6c1494f1b55c3f93e5300f69fb4ac98;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - The behavior of :paramref:`.joinedload.innerjoin` as well as :paramref:`.relationship.innerjoin` is now to use "nested" inner joins, that is, right-nested, as the default behavior when an inner join joined eager load is chained to an outer join eager load. fixes #3008 --- diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index 60a3331bf2..b0ace0d1de 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -22,6 +22,19 @@ on compatibility concerns, see :doc:`/changelog/migration_10`. + .. change:: + :tags: feature, orm + :tickets: 3008 + + The behavior of :paramref:`.joinedload.innerjoin` as well as + :paramref:`.relationship.innerjoin` is now to use "nested" + inner joins, that is, right-nested, as the default behavior when an + inner join joined eager load is chained to an outer join eager load. + + .. seealso:: + + :ref:`migration_3008` + .. change:: :tags: bug, orm :tickets: 3171 diff --git a/doc/build/changelog/migration_10.rst b/doc/build/changelog/migration_10.rst index 5124527c1f..3fb1b87636 100644 --- a/doc/build/changelog/migration_10.rst +++ b/doc/build/changelog/migration_10.rst @@ -104,6 +104,55 @@ symbol, and no change to the object's state occurs. :ticket:`3061` +.. _migration_3008: + +Right inner join nesting now the default for joinedload with innerjoin=True +--------------------------------------------------------------------------- + +The behavior of :paramref:`.joinedload.innerjoin` as well as +:paramref:`.relationship.innerjoin` is now to use "nested" +inner joins, that is, right-nested, as the default behavior when an +inner join joined eager load is chained to an outer join eager load. In +order to get the old behavior of chaining all joined eager loads as +outer join when an outer join is present, use ``innerjoin="unnested"``. + +As introduced in :ref:`feature_2976` from version 0.9, the behavior of +``innerjoin="nested"`` is that an inner join eager load chained to an outer +join eager load will use a right-nested join. ``"nested"`` is now implied +when using ``innerjoin=True``:: + + query(User).options( + joinedload("orders", innerjoin=False).joinedload("items", innerjoin=True)) + +With the new default, this will render the FROM clause in the form:: + + FROM users LEFT OUTER JOIN (orders JOIN items ON ) ON + +That is, using a right-nested join for the INNER join so that the full +result of ``users`` can be returned. The use of an INNER join is more efficient +than using an OUTER join, and allows the :paramref:`.joinedload.innerjoin` +optimization parameter to take effect in all cases. + +To get the older behavior, use ``innerjoin="unnested"``:: + + query(User).options( + joinedload("orders", innerjoin=False).joinedload("items", innerjoin="unnested")) + +This will avoid right-nested joins and chain the joins together using all +OUTER joins despite the innerjoin directive:: + + FROM users LEFT OUTER JOIN orders ON LEFT OUTER JOIN items ON + +As noted in the 0.9 notes, the only database backend that has difficulty +with right-nested joins is SQLite; SQLAlchemy as of 0.9 converts a right-nested +join into a subquery as a join target on SQLite. + +.. seealso:: + + :ref:`feature_2976` - description of the feature as introduced in 0.9.4. + +:ticket:`3008` + query.update() with ``synchronize_session='evaluate'`` raises on multi-table update ----------------------------------------------------------------------------------- diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index c2debda038..2bcb3f4a19 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -459,22 +459,18 @@ class RelationshipProperty(StrategizedProperty): nullable, or when the reference is one-to-one or a collection that is guaranteed to have one or at least one entry. - If the joined-eager load is chained onto an existing LEFT OUTER - JOIN, ``innerjoin=True`` will be bypassed and the join will continue - to chain as LEFT OUTER JOIN so that the results don't change. As an - alternative, specify the value ``"nested"``. This will instead nest - the join on the right side, e.g. using the form "a LEFT OUTER JOIN - (b JOIN c)". - - .. versionadded:: 0.9.4 Added ``innerjoin="nested"`` option to - support nesting of eager "inner" joins. + The option supports the same "nested" and "unnested" options as + that of :paramref:`.joinedload.innerjoin`. See that flag + for details on nested / unnested behaviors. .. seealso:: + :paramref:`.joinedload.innerjoin` - the option as specified by + loader option, including detail on nesting behavior. + :ref:`what_kind_of_loading` - Discussion of some details of various loader options. - :paramref:`.joinedload.innerjoin` - loader option version :param join_depth: when non-``None``, an integer value indicating how many levels diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 1e8020dd95..c3edbf6e6d 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -1324,7 +1324,8 @@ class JoinedLoader(AbstractRelationshipLoader): join_to_outer = innerjoin and isinstance(towrap, sql.Join) and \ towrap.isouter - if chained_from_outerjoin and join_to_outer and innerjoin == 'nested': + if chained_from_outerjoin and \ + join_to_outer and innerjoin != 'unnested': inner = orm_util.join( towrap.right, clauses.aliased_class, diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index 392f7cec26..4f986193e2 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -1,4 +1,3 @@ -# orm/strategy_options.py # Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # @@ -631,15 +630,47 @@ def joinedload(loadopt, attr, innerjoin=None): query(Order).options(joinedload(Order.user, innerjoin=True)) - If the joined-eager load is chained onto an existing LEFT OUTER JOIN, - ``innerjoin=True`` will be bypassed and the join will continue to - chain as LEFT OUTER JOIN so that the results don't change. As an - alternative, specify the value ``"nested"``. This will instead nest the - join on the right side, e.g. using the form "a LEFT OUTER JOIN - (b JOIN c)". + In order to chain multiple eager joins together where some may be + OUTER and others INNER, right-nested joins are used to link them:: - .. versionadded:: 0.9.4 Added ``innerjoin="nested"`` option to support - nesting of eager "inner" joins. + query(A).options( + joinedload(A.bs, innerjoin=False). + joinedload(B.cs, innerjoin=True) + ) + + The above query, linking A.bs via "outer" join and B.cs via "inner" join + would render the joins as "a LEFT OUTER JOIN (b JOIN c)". When using + SQLite, this form of JOIN is translated to use full subqueries as this + syntax is otherwise not directly supported. + + The ``innerjoin`` flag can also be stated with the term ``"unnested"``. + This will prevent joins from being right-nested, and will instead + link an "innerjoin" eagerload to an "outerjoin" eagerload by bypassing + the "inner" join. Using this form as follows:: + + query(A).options( + joinedload(A.bs, innerjoin=False). + joinedload(B.cs, innerjoin="unnested") + ) + + Joins will be rendered as "a LEFT OUTER JOIN b LEFT OUTER JOIN c", so that + all of "a" is matched rather than being incorrectly limited by a "b" that + does not contain a "c". + + .. note:: The "unnested" flag does **not** affect the JOIN rendered + from a many-to-many association table, e.g. a table configured + as :paramref:`.relationship.secondary`, to the target table; for + correctness of results, these joins are always INNER and are + therefore right-nested if linked to an OUTER join. + + .. versionadded:: 0.9.4 Added support for "nesting" of eager "inner" + joins. See :ref:`feature_2976`. + + .. versionchanged:: 1.0.0 ``innerjoin=True`` now implies + ``innerjoin="nested"``, whereas in 0.9 it implied + ``innerjoin="unnested"``. In order to achieve the pre-1.0 "unnested" + inner join behavior, use the value ``innerjoin="unnested"``. + See :ref:`migration_3008`. .. note:: diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py index ad16c83e16..a8f44e146f 100644 --- a/test/orm/test_eager_relations.py +++ b/test/orm/test_eager_relations.py @@ -879,18 +879,15 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): self.assert_compile( sess.query(User).options(joinedload(User.orders)).limit(10), - "SELECT anon_1.users_id AS anon_1_users_id, " - "anon_1.users_name AS anon_1_users_name, " - "orders_1.id AS orders_1_id, orders_1.user_id " - "AS orders_1_user_id, " - "orders_1.address_id AS " - "orders_1_address_id, orders_1.description " - "AS orders_1_description, " - "orders_1.isopen AS orders_1_isopen " + "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name " + "AS anon_1_users_name, orders_1.id AS orders_1_id, " + "orders_1.user_id AS orders_1_user_id, orders_1.address_id " + "AS orders_1_address_id, orders_1.description AS " + "orders_1_description, orders_1.isopen AS orders_1_isopen " "FROM (SELECT users.id AS users_id, users.name AS users_name " "FROM users " - "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders AS orders_1 " - "ON anon_1.users_id = orders_1.user_id", + "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders AS " + "orders_1 ON anon_1.users_id = orders_1.user_id", {'param_1': 10} ) @@ -969,22 +966,20 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): joinedload( "orders.address", innerjoin=True)).limit(10), - "SELECT anon_1.users_id AS anon_1_users_id, " - "anon_1.users_name AS anon_1_users_name, " - "addresses_1.id AS addresses_1_id, " + "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name " + "AS anon_1_users_name, addresses_1.id AS addresses_1_id, " "addresses_1.user_id AS addresses_1_user_id, " "addresses_1.email_address AS addresses_1_email_address, " - "orders_1.id AS orders_1_id, " - "orders_1.user_id AS orders_1_user_id, " - "orders_1.address_id AS orders_1_address_id, " + "orders_1.id AS orders_1_id, orders_1.user_id AS " + "orders_1_user_id, orders_1.address_id AS orders_1_address_id, " "orders_1.description AS orders_1_description, " "orders_1.isopen AS orders_1_isopen " "FROM (SELECT users.id AS users_id, users.name AS users_name " - "FROM users " - "LIMIT :param_1) AS anon_1 LEFT OUTER JOIN orders " - "AS orders_1 ON anon_1.users_id = " - "orders_1.user_id LEFT OUTER JOIN addresses AS addresses_1 " - "ON addresses_1.id = orders_1.address_id", + "FROM users" + " LIMIT :param_1) AS anon_1 LEFT OUTER JOIN " + "(orders AS orders_1 JOIN addresses AS addresses_1 " + "ON addresses_1.id = orders_1.address_id) ON " + "anon_1.users_id = orders_1.user_id", {'param_1': 10} ) @@ -1291,7 +1286,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): "addresses AS addresses_1 ON users.id = addresses_1.user_id " "ORDER BY addresses_1.id") - def test_inner_join_chaining_options(self): + def test_inner_join_unnested_chaining_options(self): users, items, order_items, Order, Item, User, orders = ( self.tables.users, self.tables.items, @@ -1302,12 +1297,12 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): self.tables.orders) mapper(User, users, properties=dict( - orders=relationship(Order, innerjoin=True, + orders=relationship(Order, innerjoin="unnested", lazy=False) )) mapper(Order, orders, properties=dict( items=relationship(Item, secondary=order_items, lazy=False, - innerjoin=True) + innerjoin="unnested") )) mapper(Item, items) @@ -1382,12 +1377,12 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): self.tables.orders) mapper(User, users, properties=dict( - orders=relationship(Order, innerjoin='nested', + orders=relationship(Order, innerjoin=True, lazy=False, order_by=orders.c.id) )) mapper(Order, orders, properties=dict( items=relationship(Item, secondary=order_items, lazy=False, - innerjoin='nested', order_by=items.c.id) + innerjoin=True, order_by=items.c.id) )) mapper(Item, items) @@ -1505,7 +1500,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): sess = create_session() q = sess.query(User).options( joinedload("orders", innerjoin=False). - joinedload("items", innerjoin="nested") + joinedload("items", innerjoin=True) ) self.assert_compile( @@ -1572,7 +1567,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): sess = create_session() q = sess.query(User).options( joinedload("orders"), - joinedload("addresses", innerjoin=True), + joinedload("addresses", innerjoin="unnested"), ) self.assert_compile( @@ -1608,7 +1603,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): sess = create_session() q = sess.query(User).options( joinedload("orders"), - joinedload("addresses", innerjoin='nested'), + joinedload("addresses", innerjoin=True), ) self.assert_compile( @@ -1692,7 +1687,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): "ORDER BY items_1.id, keywords_1.id" ) - def test_inner_join_chaining_fixed(self): + def test_inner_join_unnested_chaining_fixed(self): users, items, order_items, Order, Item, User, orders = ( self.tables.users, self.tables.items, @@ -1707,7 +1702,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): )) mapper(Order, orders, properties=dict( items=relationship(Item, secondary=order_items, lazy=False, - innerjoin=True) + innerjoin="unnested") )) mapper(Item, items)