]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The behavior of :paramref:`.joinedload.innerjoin` as well as
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 26 Aug 2014 21:23:23 +0000 (17:23 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 26 Aug 2014 21:23:23 +0000 (17:23 -0400)
: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

doc/build/changelog/changelog_10.rst
doc/build/changelog/migration_10.rst
lib/sqlalchemy/orm/relationships.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/strategy_options.py
test/orm/test_eager_relations.py

index 60a3331bf2359209a9a73b97d09da5c3c264296a..b0ace0d1de7fc0b3edde69558ca04c5dc5785cdc 100644 (file)
     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
index 5124527c1f7f75bf18fd534b263b295c16b9056e..3fb1b87636d2b62ac324924238b5bf9e98c3559d 100644 (file)
@@ -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 <onclause>) ON <onclause>
+
+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 <onclause> LEFT OUTER JOIN items ON <onclause>
+
+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
 -----------------------------------------------------------------------------------
 
index c2debda038a4f655439aa939cf55ae9f112625cb..2bcb3f4a19e3d5be44998ebc757ebbd9c61bc383 100644 (file)
@@ -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
index 1e8020dd951e8a5de55ae7547b519efd13f2a03c..c3edbf6e6d9bf914a3bb9289d24eadd813ae83d8 100644 (file)
@@ -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,
index 392f7cec26ab4c52491d2a00aefc7ea52f2351a7..4f986193e24de4fc8c50177e88045f63f8427a69 100644 (file)
@@ -1,4 +1,3 @@
-# orm/strategy_options.py
 # Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
 # <see AUTHORS file>
 #
@@ -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::
 
index ad16c83e164e52432424fe0fee0f954118384765..a8f44e146f2aa43248c3788b47d5aed33e8e7826 100644 (file)
@@ -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)