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
: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
-----------------------------------------------------------------------------------
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
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,
-# orm/strategy_options.py
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
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::
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}
)
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}
)
"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,
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)
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)
sess = create_session()
q = sess.query(User).options(
joinedload("orders", innerjoin=False).
- joinedload("items", innerjoin="nested")
+ joinedload("items", innerjoin=True)
)
self.assert_compile(
sess = create_session()
q = sess.query(User).options(
joinedload("orders"),
- joinedload("addresses", innerjoin=True),
+ joinedload("addresses", innerjoin="unnested"),
)
self.assert_compile(
sess = create_session()
q = sess.query(User).options(
joinedload("orders"),
- joinedload("addresses", innerjoin='nested'),
+ joinedload("addresses", innerjoin=True),
)
self.assert_compile(
"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,
))
mapper(Order, orders, properties=dict(
items=relationship(Item, secondary=order_items, lazy=False,
- innerjoin=True)
+ innerjoin="unnested")
))
mapper(Item, items)