]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Consider aliased=True, from_joinpoint as legacy
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 4 May 2020 19:48:48 +0000 (15:48 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 4 May 2020 20:01:53 +0000 (16:01 -0400)
For a 1.4 / 1.3 merge, rewrite the documentation for
Query.join() to indicate calling forms that are now considered
legacy, including the use of strings in join(), sending a
series of join paths in one call, and using the aliased=True
flag.   update the elementtree examples as well to use aliased()
(they are much simpler to understand this way too) and update
other links.

Also improve docs for aliased() and some other ORM targets
such as PropComparator.

Change-Id: I636e3a9130dc5509e51c2cf60a52f38fcadffbc6
References: #4705

doc/build/conf.py
doc/build/orm/internals.rst
doc/build/orm/self_referential.rst
doc/build/orm/tutorial.rst
examples/elementtree/adjacency_list.py
examples/elementtree/optimized_al.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/util.py

index d4dfd18ce599bf171ed0bc4273dbd020108dfc71..4ebb93b85c87e7f329c70e46bf2472a8a3c30afc 100644 (file)
@@ -107,6 +107,8 @@ autodocmods_convert_modname = {
     "sqlalchemy.engine.cursor": "sqlalchemy.engine",
     "sqlalchemy.engine.result": "sqlalchemy.engine",
     "sqlalchemy.util._collections": "sqlalchemy.util",
+    "sqlalchemy.orm.relationships": "sqlalchemy.orm",
+    "sqlalchemy.orm.interfaces": "sqlalchemy.orm",
 }
 
 autodocmods_convert_modname_w_class = {
index e9f1b431eaf952fc34948452a17f69a4e9128aea..c9683e1458813524554ac7f66634524352d2da3a 100644 (file)
@@ -18,7 +18,7 @@ sections, are listed here.
     :members:
     :inherited-members:
 
-.. autoclass:: sqlalchemy.orm.properties.ColumnProperty
+.. autoclass:: sqlalchemy.orm.ColumnProperty
     :members:
 
     .. attribute:: Comparator.expressions
@@ -32,7 +32,7 @@ sections, are listed here.
 
             :ref:`maptojoin` - usage example
 
-.. autoclass:: sqlalchemy.orm.descriptor_props.CompositeProperty
+.. autoclass:: sqlalchemy.orm.CompositeProperty
     :members:
 
 
@@ -88,15 +88,15 @@ sections, are listed here.
 
 .. autodata:: sqlalchemy.orm.interfaces.ONETOMANY
 
-.. autoclass:: sqlalchemy.orm.interfaces.PropComparator
+.. autoclass:: sqlalchemy.orm.PropComparator
     :members:
     :inherited-members:
 
-.. autoclass:: sqlalchemy.orm.relationships.RelationshipProperty
+.. autoclass:: sqlalchemy.orm.RelationshipProperty
     :members:
     :inherited-members:
 
-.. autoclass:: sqlalchemy.orm.descriptor_props.SynonymProperty
+.. autoclass:: sqlalchemy.orm.SynonymProperty
     :members:
     :inherited-members:
 
index 4f656e44b9d1231432702ab758a49323a6b2cce3..739b6e06877ef5fe53e709224d2cba95f475fe36 100644 (file)
@@ -138,7 +138,7 @@ looks like:
 
     nodealias = aliased(Node)
     {sql}session.query(Node).filter(Node.data=='subchild1').\
-                    join(nodealias, Node.parent).\
+                    join(Node.parent.of_type(nodealias)).\
                     filter(nodealias.data=="child2").\
                     all()
     SELECT node.id AS node_id,
@@ -150,66 +150,8 @@ looks like:
         AND node_1.data = ?
     ['subchild1', 'child2']
 
-:meth:`_query.Query.join` also includes a feature known as
-:paramref:`.Query.join.aliased` that can shorten the verbosity self-
-referential joins, at the expense of query flexibility.  This feature
-performs a similar "aliasing" step to that above, without the need for
-an explicit entity.   Calls to :meth:`_query.Query.filter` and similar
-subsequent to the aliased join will **adapt** the ``Node`` entity to
-be that of the alias:
-
-.. sourcecode:: python+sql
-
-    {sql}session.query(Node).filter(Node.data=='subchild1').\
-            join(Node.parent, aliased=True).\
-            filter(Node.data=='child2').\
-            all()
-    SELECT node.id AS node_id,
-            node.parent_id AS node_parent_id,
-            node.data AS node_data
-    FROM node
-        JOIN node AS node_1 ON node_1.id = node.parent_id
-    WHERE node.data = ? AND node_1.data = ?
-    ['subchild1', 'child2']
-
-To add criterion to multiple points along a longer join, add
-:paramref:`.Query.join.from_joinpoint` to the additional
-:meth:`_query.Query.join` calls:
-
-.. sourcecode:: python+sql
-
-    # get all nodes named 'subchild1' with a
-    # parent named 'child2' and a grandparent 'root'
-    {sql}session.query(Node).\
-            filter(Node.data=='subchild1').\
-            join(Node.parent, aliased=True).\
-            filter(Node.data=='child2').\
-            join(Node.parent, aliased=True, from_joinpoint=True).\
-            filter(Node.data=='root').\
-            all()
-    SELECT node.id AS node_id,
-            node.parent_id AS node_parent_id,
-            node.data AS node_data
-    FROM node
-        JOIN node AS node_1 ON node_1.id = node.parent_id
-        JOIN node AS node_2 ON node_2.id = node_1.parent_id
-    WHERE node.data = ?
-        AND node_1.data = ?
-        AND node_2.data = ?
-    ['subchild1', 'child2', 'root']
-
-:meth:`_query.Query.reset_joinpoint` will also remove the "aliasing" from filtering
-calls::
-
-    session.query(Node).\
-            join(Node.children, aliased=True).\
-            filter(Node.data == 'foo').\
-            reset_joinpoint().\
-            filter(Node.data == 'bar')
-
-For an example of using :paramref:`.Query.join.aliased` to
-arbitrarily join along a chain of self-referential nodes, see
-:ref:`examples_xmlpersistence`.
+For an example of using :func:`_orm.aliased` to join across an arbitrarily long
+chain of self-referential nodes, see :ref:`examples_xmlpersistence`.
 
 .. _self_referential_eager_loading:
 
index 85638f6220706ae8e7fc8f181e75533b4b9e0ff7..e77964a7b6c314287b8e8fdacedd5ca05b702138 100644 (file)
@@ -1379,7 +1379,6 @@ works better when one of the following forms are used::
     query.join(Address, User.id==Address.user_id)    # explicit condition
     query.join(User.addresses)                       # specify relationship from left to right
     query.join(Address, User.addresses)              # same, with explicit target
-    query.join('addresses')                          # same, using a string
 
 As you would expect, the same idea is used for "outer" joins, using the
 :meth:`_query.Query.outerjoin` function::
@@ -1408,10 +1407,13 @@ Using Aliases
 When querying across multiple tables, if the same table needs to be referenced
 more than once, SQL typically requires that the table be *aliased* with
 another name, so that it can be distinguished against other occurrences of
-that table. The :class:`~sqlalchemy.orm.query.Query` supports this most
-explicitly using the :attr:`~sqlalchemy.orm.aliased` construct. Below we join to the ``Address``
-entity twice, to locate a user who has two distinct email addresses at the
-same time:
+that table.   This is supported using the
+:func:`_orm.aliased` construct.   When joining to relationships using
+using :func:`_orm.aliased`, the special attribute method
+:meth:`_orm.PropComparator.of_type` may be used to alter the target of
+a relationship join to refer to a given :func:`_orm.aliased` object.
+Below we join to the ``Address`` entity twice, to locate a user who has two
+distinct email addresses at the same time:
 
 .. sourcecode:: python+sql
 
@@ -1420,8 +1422,8 @@ same time:
     >>> adalias2 = aliased(Address)
     {sql}>>> for username, email1, email2 in \
     ...     session.query(User.name, adalias1.email_address, adalias2.email_address).\
-    ...     join(adalias1, User.addresses).\
-    ...     join(adalias2, User.addresses).\
+    ...     join(User.addresses.of_type(adalias1)).\
+    ...     join(User.addresses.of_type(adalias2)).\
     ...     filter(adalias1.email_address=='jack@google.com').\
     ...     filter(adalias2.email_address=='j25@yahoo.com'):
     ...     print(username, email1, email2)
@@ -1437,6 +1439,13 @@ same time:
     ('jack@google.com', 'j25@yahoo.com')
     {stop}jack jack@google.com j25@yahoo.com
 
+In addition to using the :meth:`_orm.PropComparator.of_type` method, it is
+common to see the :meth:`_orm.Query.join` method joining to a specific
+target by indicating it separately::
+
+    # equivalent to query.join(User.addresses.of_type(adalias1))
+    q = query.join(adalias1, User.addresses)
+
 Using Subqueries
 ----------------
 
index 0a5a922d0ab326496ff105fab6ed06e2e545fe31..cee73bffd51163ac9a93e2a8fa6b5c858332edda 100644 (file)
@@ -33,6 +33,7 @@ from sqlalchemy import MetaData
 from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import Unicode
+from sqlalchemy.orm import aliased
 from sqlalchemy.orm import lazyload
 from sqlalchemy.orm import mapper
 from sqlalchemy.orm import relationship
@@ -215,14 +216,20 @@ ElementTree.dump(document.element)
 # PART VI - Searching for Paths
 
 # manually search for a document which contains "/somefile/header/field1:hi"
+root = aliased(_Node)
+child_node = aliased(_Node)
+grandchild_node = aliased(_Node)
+
 d = (
     session.query(Document)
-    .join("_root", aliased=True)
-    .filter(_Node.tag == "somefile")
-    .join("children", aliased=True, from_joinpoint=True)
-    .filter(_Node.tag == "header")
-    .join("children", aliased=True, from_joinpoint=True)
-    .filter(and_(_Node.tag == "field1", _Node.text == "hi"))
+    .join(Document._root.of_type(root))
+    .filter(root.tag == "somefile")
+    .join(root.children.of_type(child_node))
+    .filter(child_node.tag == "header")
+    .join(child_node.children.of_type(grandchild_node))
+    .filter(
+        and_(grandchild_node.tag == "field1", grandchild_node.text == "hi")
+    )
     .one()
 )
 ElementTree.dump(d.element)
@@ -232,31 +239,39 @@ ElementTree.dump(d.element)
 
 def find_document(path, compareto):
     query = session.query(Document)
-    attribute = "_root"
+    attribute = Document._root
     for i, match in enumerate(
         re.finditer(r"/([\w_]+)(?:\[@([\w_]+)(?:=(.*))?\])?", path)
     ):
         (token, attrname, attrvalue) = match.group(1, 2, 3)
-        query = query.join(
-            attribute, aliased=True, from_joinpoint=True
-        ).filter(_Node.tag == token)
-        attribute = "children"
+        target_node = aliased(_Node)
+
+        query = query.join(attribute.of_type(target_node)).filter(
+            target_node.tag == token
+        )
+
+        attribute = target_node.children
+
         if attrname:
+            attribute_entity = aliased(_Attribute)
+
             if attrvalue:
                 query = query.join(
-                    "attributes", aliased=True, from_joinpoint=True
+                    target_node.attributes.of_type(attribute_entity)
                 ).filter(
                     and_(
-                        _Attribute.name == attrname,
-                        _Attribute.value == attrvalue,
+                        attribute_entity.name == attrname,
+                        attribute_entity.value == attrvalue,
                     )
                 )
             else:
                 query = query.join(
-                    "attributes", aliased=True, from_joinpoint=True
-                ).filter(_Attribute.name == attrname)
+                    target_node.attributes.of_type(attribute_entity)
+                ).filter(attribute_entity.name == attrname)
     return (
-        query.options(lazyload("_root")).filter(_Node.text == compareto).all()
+        query.options(lazyload(Document._root))
+        .filter(target_node.text == compareto)
+        .all()
     )
 
 
index 0ba2d1ea4ebff2a09abe39a818818c42ba0232d6..158b335fddd941cf7402cdbad4fdc62740ffc40f 100644 (file)
@@ -24,6 +24,7 @@ from sqlalchemy import MetaData
 from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import Unicode
+from sqlalchemy.orm import aliased
 from sqlalchemy.orm import lazyload
 from sqlalchemy.orm import mapper
 from sqlalchemy.orm import relationship
@@ -223,14 +224,21 @@ ElementTree.dump(document.element)
 
 # manually search for a document which contains "/somefile/header/field1:hi"
 print("\nManual search for /somefile/header/field1=='hi':", line)
+
+root = aliased(_Node)
+child_node = aliased(_Node)
+grandchild_node = aliased(_Node)
+
 d = (
     session.query(Document)
-    .join("_nodes", aliased=True)
-    .filter(and_(_Node.parent_id == None, _Node.tag == "somefile"))
-    .join("children", aliased=True, from_joinpoint=True)
-    .filter(_Node.tag == "header")
-    .join("children", aliased=True, from_joinpoint=True)
-    .filter(and_(_Node.tag == "field1", _Node.text == "hi"))
+    .join(Document._nodes.of_type(root))
+    .filter(and_(root.parent_id.is_(None), root.tag == "somefile"))
+    .join(root.children.of_type(child_node))
+    .filter(child_node.tag == "header")
+    .join(child_node.children.of_type(grandchild_node))
+    .filter(
+        and_(grandchild_node.tag == "field1", grandchild_node.text == "hi")
+    )
     .one()
 )
 ElementTree.dump(d.element)
@@ -240,32 +248,44 @@ ElementTree.dump(d.element)
 
 def find_document(path, compareto):
     query = session.query(Document)
-    first = True
+
     for i, match in enumerate(
         re.finditer(r"/([\w_]+)(?:\[@([\w_]+)(?:=(.*))?\])?", path)
     ):
         (token, attrname, attrvalue) = match.group(1, 2, 3)
-        if first:
-            query = query.join("_nodes", aliased=True).filter(
-                _Node.parent_id == None
+
+        if not i:
+            parent = Document
+            target_node = aliased(_Node)
+
+            query = query.join(parent._nodes.of_type(target_node)).filter(
+                target_node.parent_id.is_(None)
             )
-            first = False
         else:
-            query = query.join("children", aliased=True, from_joinpoint=True)
-        query = query.filter(_Node.tag == token)
+            parent = target_node
+            target_node = aliased(_Node)
+
+            query = query.join(parent.children.of_type(target_node))
+
+        query = query.filter(target_node.tag == token)
         if attrname:
-            query = query.join("attributes", aliased=True, from_joinpoint=True)
+            attribute_entity = aliased(_Attribute)
+            query = query.join(
+                target_node.attributes.of_type(attribute_entity)
+            )
             if attrvalue:
                 query = query.filter(
                     and_(
-                        _Attribute.name == attrname,
-                        _Attribute.value == attrvalue,
+                        attribute_entity.name == attrname,
+                        attribute_entity.value == attrvalue,
                     )
                 )
             else:
-                query = query.filter(_Attribute.name == attrname)
+                query = query.filter(attribute_entity.name == attrname)
     return (
-        query.options(lazyload("_nodes")).filter(_Node.text == compareto).all()
+        query.options(lazyload(Document._nodes))
+        .filter(target_node.text == compareto)
+        .all()
     )
 
 
index 5588828eb397876baac3e491c2deda5ff59b6dbe..6e22a6904485a5da5e08476088906510a2a9d014 100644 (file)
@@ -1985,31 +1985,19 @@ class Query(Generative):
 
             q = session.query(User).join(User.addresses)
 
-        Where above, the call to :meth:`_query.Query.join` along ``User.
-        addresses``
-        will result in SQL equivalent to::
+        Where above, the call to :meth:`_query.Query.join` along
+        ``User.addresses`` will result in SQL approximately equivalent to::
 
-            SELECT user.* FROM user JOIN address ON user.id = address.user_id
+            SELECT user.id, User.name
+            FROM user JOIN address ON user.id = address.user_id
 
         In the above example we refer to ``User.addresses`` as passed to
-        :meth:`_query.Query.join` as the *on clause*, that is, it indicates
-        how the "ON" portion of the JOIN should be constructed.  For a
-        single-entity query such as the one above (i.e. we start by selecting
-        only from ``User`` and nothing else), the relationship can also be
-        specified by its string name::
+        :meth:`_query.Query.join` as the "on clause", that is, it indicates
+        how the "ON" portion of the JOIN should be constructed.
 
-            q = session.query(User).join("addresses")
-
-        :meth:`_query.Query.join` can also accommodate multiple
-        "on clause" arguments to produce a chain of joins, such as below
-        where a join across four related entities is constructed::
-
-            q = session.query(User).join("orders", "items", "keywords")
-
-        The above would be shorthand for three separate calls to
-        :meth:`_query.Query.join`,
-        each using an explicit attribute to indicate
-        the source entity::
+        To construct a chain of joins, multiple :meth:`_query.Query.join`
+        calls may be used.  The relationship-bound attribute implies both
+        the left and right side of the join at once::
 
             q = session.query(User).\
                     join(User.orders).\
@@ -2018,114 +2006,99 @@ class Query(Generative):
 
         **Joins to a Target Entity or Selectable**
 
-        A second form of :meth:`_query.Query.join` allows any mapped entity
-        or core selectable construct as a target.   In this usage,
-        :meth:`_query.Query.join` will attempt
-        to create a JOIN along the natural foreign key relationship between
-        two entities::
+        A second form of :meth:`_query.Query.join` allows any mapped entity or
+        core selectable construct as a target.   In this usage,
+        :meth:`_query.Query.join` will attempt to create a JOIN along the
+        natural foreign key relationship between two entities::
 
             q = session.query(User).join(Address)
 
-        The above calling form of :meth:`_query.Query.join`
-        will raise an error if
-        either there are no foreign keys between the two entities, or if
-        there are multiple foreign key linkages between them.   In the
-        above calling form, :meth:`_query.Query.join` is called upon to
-        create the "on clause" automatically for us.  The target can
-        be any mapped entity or selectable, such as a :class:`_schema.Table`::
-
-            q = session.query(User).join(addresses_table)
+        In the above calling form, :meth:`_query.Query.join` is called upon to
+        create the "on clause" automatically for us.  This calling form will
+        ultimately raise an error if either there are no foreign keys between
+        the two entities, or if there are multiple foreign key linkages between
+        the target entity and the entity or entities already present on the
+        left side such that creating a join requires more information.  Note
+        that when indicating a join to a target without any ON clause, ORM
+        configured relationships are not taken into account.
 
         **Joins to a Target with an ON Clause**
 
         The third calling form allows both the target entity as well
-        as the ON clause to be passed explicitly.   Suppose for
-        example we wanted to join to ``Address`` twice, using
-        an alias the second time.  We use :func:`~sqlalchemy.orm.aliased`
-        to create a distinct alias of ``Address``, and join
-        to it using the ``target, onclause`` form, so that the
-        alias can be specified explicitly as the target along with
-        the relationship to instruct how the ON clause should proceed::
+        as the ON clause to be passed explicitly.    A example that includes
+        a SQL expression as the ON clause is as follows::
 
-            a_alias = aliased(Address)
+            q = session.query(User).join(Address, User.id==Address.user_id)
 
-            q = session.query(User).\
-                    join(User.addresses).\
-                    join(a_alias, User.addresses).\
-                    filter(Address.email_address=='ed@foo.com').\
-                    filter(a_alias.email_address=='ed@bar.com')
-
-        Where above, the generated SQL would be similar to::
-
-            SELECT user.* FROM user
-                JOIN address ON user.id = address.user_id
-                JOIN address AS address_1 ON user.id=address_1.user_id
-                WHERE address.email_address = :email_address_1
-                AND address_1.email_address = :email_address_2
-
-        The two-argument calling form of :meth:`_query.Query.join`
-        also allows us to construct arbitrary joins with SQL-oriented
-        "on clause" expressions, not relying upon configured relationships
-        at all.  Any SQL expression can be passed as the ON clause
-        when using the two-argument form, which should refer to the target
-        entity in some way as well as an applicable source entity::
+        The above form may also use a relationship-bound attribute as the
+        ON clause as well::
 
-            q = session.query(User).join(Address, User.id==Address.user_id)
+            q = session.query(User).join(Address, User.addresses)
 
-        **Advanced Join Targeting and Adaption**
+        The above syntax can be useful for the case where we wish
+        to join to an alias of a particular target entity.  If we wanted
+        to join to ``Address`` twice, it could be achieved using two
+        aliases set up using the :func:`~sqlalchemy.orm.aliased` function::
 
-        There is a lot of flexibility in what the "target" can be when using
-        :meth:`_query.Query.join`.   As noted previously, it also accepts
-        :class:`_schema.Table` constructs and other selectables such as
-        :func:`.alias`
-        and :func:`_expression.select` constructs, with either the one or
-        two-argument forms::
+            a1 = aliased(Address)
+            a2 = aliased(Address)
 
-            addresses_q = select([Address.user_id]).\
-                        where(Address.email_address.endswith("@bar.com")).\
-                        alias()
+            q = session.query(User).\
+                    join(a1, User.addresses).\
+                    join(a2, User.addresses).\
+                    filter(a1.email_address=='ed@foo.com').\
+                    filter(a2.email_address=='ed@bar.com')
+
+        The relationship-bound calling form can also specify a target entity
+        using the :meth:`_orm.PropComparator.of_type` method; a query
+        equivalent to the one above would be::
+
+            a1 = aliased(Address)
+            a2 = aliased(Address)
 
             q = session.query(User).\
-                        join(addresses_q, addresses_q.c.user_id==User.id)
+                    join(User.addresses.of_type(a1)).\
+                    join(User.addresses.of_type(a2)).\
+                    filter(a1.email_address == 'ed@foo.com').\
+                    filter(a2.email_address == 'ed@bar.com')
 
-        :meth:`_query.Query.join` also features the ability to *adapt* a
-        :meth:`~sqlalchemy.orm.relationship` -driven ON clause to the target
-        selectable. Below we construct a JOIN from ``User`` to a subquery
-        against ``Address``, allowing the relationship denoted by
-        ``User.addresses`` to *adapt* itself to the altered target::
+        **Joining to Tables and Subqueries**
 
-            address_subq = session.query(Address).\
-                            filter(Address.email_address == 'ed@foo.com').\
-                            subquery()
 
-            q = session.query(User).join(address_subq, User.addresses)
+        The target of a join may also be any table or SELECT statement,
+        which may be related to a target entity or not.   Use the
+        appropriate ``.subquery()`` method in order to make a subquery
+        out of a query::
 
-        Producing SQL similar to::
+            subq = session.query(Address).\
+                filter(Address.email_address == 'ed@foo.com').\
+                subquery()
 
-            SELECT user.* FROM user
-                JOIN (
-                    SELECT address.id AS id,
-                            address.user_id AS user_id,
-                            address.email_address AS email_address
-                    FROM address
-                    WHERE address.email_address = :email_address_1
-                ) AS anon_1 ON user.id = anon_1.user_id
 
-        The above form allows one to fall back onto an explicit ON
-        clause at any time::
+            q = session.query(User).join(
+                subq, User.id == subq.c.user_id
+            )
+
+        Joining to a subquery in terms of a specific relationship and/or
+        target entity may be achieved by linking the subquery to the
+        entity using :func:`_orm.aliased`::
+
+            subq = session.query(Address).\
+                filter(Address.email_address == 'ed@foo.com').\
+                subquery()
+
+            address_subq = aliased(Address, subq)
+
+            q = session.query(User).join(
+                User.addresses.of_type(address_subq)
+            )
 
-            q = session.query(User).\
-                    join(address_subq, User.id==address_subq.c.user_id)
 
         **Controlling what to Join From**
 
-        While :meth:`_query.Query.join` exclusively deals with the "right"
-        side of the JOIN, we can also control the "left" side, in those
-        cases where it's needed, using :meth:`_query.Query.select_from`.
-        Below we construct a query against ``Address`` but can still
-        make usage of ``User.addresses`` as our ON clause by instructing
-        the :class:`_query.Query` to select first from the ``User``
-        entity::
+        In cases where the left side of the current state of
+        :class:`_query.Query` is not in line with what we want to join from,
+        the :meth:`_query.Query.select_from` method may be used::
 
             q = session.query(Address).select_from(User).\
                             join(User.addresses).\
@@ -2137,67 +2110,94 @@ class Query(Generative):
                 JOIN address ON user.id=address.user_id
                 WHERE user.name = :name_1
 
-        **Constructing Aliases Anonymously**
-
-        :meth:`_query.Query.join` can construct anonymous aliases
-        using the ``aliased=True`` flag.  This feature is useful
-        when a query is being joined algorithmically, such as
-        when querying self-referentially to an arbitrary depth::
-
-            q = session.query(Node).\
-                    join("children", "children", aliased=True)
-
-        When ``aliased=True`` is used, the actual "alias" construct
-        is not explicitly available.  To work with it, methods such as
-        :meth:`_query.Query.filter` will adapt the incoming entity to
-        the last join point::
-
-            q = session.query(Node).\
-                    join("children", "children", aliased=True).\
-                    filter(Node.name == 'grandchild 1')
-
-        When using automatic aliasing, the ``from_joinpoint=True``
-        argument can allow a multi-node join to be broken into
-        multiple calls to :meth:`_query.Query.join`, so that
-        each path along the way can be further filtered::
-
-            q = session.query(Node).\
-                    join("children", aliased=True).\
-                    filter(Node.name='child 1').\
-                    join("children", aliased=True, from_joinpoint=True).\
-                    filter(Node.name == 'grandchild 1')
-
-        The filtering aliases above can then be reset back to the
-        original ``Node`` entity using :meth:`_query.Query.reset_joinpoint`::
-
-            q = session.query(Node).\
-                    join("children", "children", aliased=True).\
-                    filter(Node.name == 'grandchild 1').\
-                    reset_joinpoint().\
-                    filter(Node.name == 'parent 1)
-
-        For an example of ``aliased=True``, see the distribution
-        example :ref:`examples_xmlpersistence` which illustrates
-        an XPath-like query system using algorithmic joins.
-
-        :param \*props: A collection of one or more join conditions,
-         each consisting of a relationship-bound attribute or string
-         relationship name representing an "on clause", or a single
-         target entity, or a tuple in the form of ``(target, onclause)``.
-         A special two-argument calling form of the form ``target, onclause``
-         is also accepted.
-        :param aliased=False: If True, indicate that the JOIN target should be
-         anonymously aliased.  Subsequent calls to :meth:`_query.Query.filter`
-         and similar will adapt the incoming criterion to the target
-         alias, until :meth:`_query.Query.reset_joinpoint` is called.
-        :param isouter=False: If True, the join used will be a left outer join,
-         just as if the :meth:`_query.Query.outerjoin` method were called.
-         This
-         flag is here to maintain consistency with the same flag as accepted
-         by :meth:`_expression.FromClause.join` and other Core constructs.
+        **Legacy Features of Query.join()**
 
+        The :meth:`_query.Query.join` method currently supports several
+        usage patterns and arguments that are considered to be legacy
+        as of SQLAlchemy 1.3.   A deprecation path will follow
+        in the 1.4 series for the following features:
 
-         .. versionadded:: 1.0.0
+
+        * Joining on relationship names rather than attributes::
+
+            session.query(User).join("addresses")
+
+          **Why it's legacy**: the string name does not provide enough context
+          for :meth:`_query.Query.join` to always know what is desired,
+          notably in that there is no indication of what the left side
+          of the join should be.  This gives rise to flags like
+          ``from_joinpoint`` as well as the ability to place several
+          join clauses in a single :meth:`_query.Query.join` call
+          which don't solve the problem fully while also
+          adding new calling styles that are unnecessary and expensive to
+          accommodate internally.
+
+          **Modern calling pattern**:  Use the actual relationship,
+          e.g. ``User.addresses`` in the above case::
+
+              session.query(User).join(User.addresses)
+
+        * Automatic aliasing with the ``aliased=True`` flag::
+
+            session.query(Node).join(Node.children, aliased=True).\
+                filter(Node.name == 'some name')
+
+          **Why it's legacy**:  the automatic aliasing feature of
+          :class:`_query.Query` is intensely complicated, both in its internal
+          implementation as well as in its observed behavior, and is almost
+          never used.  It is difficult to know upon inspection where and when
+          its aliasing of a target entity, ``Node`` in the above case, will be
+          applied and when it won't, and additionally the feature has to use
+          very elaborate heuristics to achieve this implicit behavior.
+
+          **Modern calling pattern**: Use the :func:`_orm.aliased` construct
+          explicitly::
+
+            from sqlalchemy.orm import aliased
+
+            n1 = aliased(Node)
+
+            session.query(Node).join(Node.children.of_type(n1)).\
+                filter(n1.name == 'some name')
+
+        * Multiple joins in one call::
+
+            session.query(User).join("orders", "items")
+
+            session.query(User).join(User.orders, Order.items)
+
+            session.query(User).join(
+                (Order, User.orders),
+                (Item, Item.order_id == Order.id)
+            )
+
+            # ... and several more forms actually
+
+          **Why it's legacy**: being able to chain multiple ON clauses in one
+          call to :meth:`_query.Query.join` is yet another attempt to solve
+          the problem of being able to specify what entity to join from,
+          and is the source of a large variety of potential calling patterns
+          that are internally expensive and complicated to parse and
+          accommodate.
+
+          **Modern calling pattern**:  Use relationship-bound attributes
+          or SQL-oriented ON clauses within separate calls, so that
+          each call to :meth:`_query.Query.join` knows what the left
+          side should be::
+
+            session.query(User).join(User.orders).join(
+                Item, Item.order_id == Order.id)
+
+
+        :param \*props: Incoming arguments for :meth:`_query.Query.join`,
+         the props collection in modern use should be considered to be a  one
+         or two argument form, either as a single "target" entity or ORM
+         attribute-bound relationship, or as a target entity plus an "on
+         clause" which  may be a SQL expression or ORM attribute-bound
+         relationship.
+
+        :param isouter=False: If True, the join used will be a left outer join,
+         just as if the :meth:`_query.Query.outerjoin` method were called.
 
         :param full=False: render FULL OUTER JOIN; implies ``isouter``.
 
@@ -2208,6 +2208,15 @@ class Query(Generative):
          joined target, rather than starting back from the original
          FROM clauses of the query.
 
+         .. note:: This flag is considered legacy.
+
+        :param aliased=False: If True, indicate that the JOIN target should be
+         anonymously aliased.  Subsequent calls to :meth:`_query.Query.filter`
+         and similar will adapt the incoming criterion to the target
+         alias, until :meth:`_query.Query.reset_joinpoint` is called.
+
+         .. note:: This flag is considered legacy.
+
         .. seealso::
 
             :ref:`ormtutorial_joins` in the ORM tutorial.
index 3558a7c5fd320ed2ad0b7198ab7167d0d5f3f84a..8513374a8dfe917c52ed7540576e2b956089ccb3 100644 (file)
@@ -796,12 +796,13 @@ def aliased(element, alias=None, name=None, flat=False, adapt_on_names=False):
      but for convenience can also be a :class:`_expression.FromClause` element
      .
 
-    :param alias: Optional selectable unit to map the element to.  This should
-     normally be a :class:`_expression.Alias` object corresponding to the
-     :class:`_schema.Table`
-     to which the class is mapped, or to a :func:`_expression.select`
-     construct that is compatible with the mapping.   By default, a simple
-     anonymous alias of the mapped table is generated.
+    :param alias: Optional selectable unit to map the element to.  This is
+     usually used to link the object to a subquery, and should be an aliased
+     select construct as one would produce from the
+     :meth:`_query.Query.subquery` method or
+     the :meth:`_expression.Select.subquery` or
+     :meth:`_expression.Select.alias` methods of the :func:`_expression.select`
+     construct.
 
     :param name: optional string name to use for the alias, if not specified
      by the ``alias`` parameter.  The name, among other things, forms the
@@ -816,8 +817,6 @@ def aliased(element, alias=None, name=None, flat=False, adapt_on_names=False):
      rewritten as a JOIN against an aliased SELECT subquery on backends that
      don't support this syntax.
 
-     .. versionadded:: 0.9.0
-
      .. seealso:: :meth:`_expression.Join.alias`
 
     :param adapt_on_names: if True, more liberal "matching" will be used when