summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/conf.py2
-rw-r--r--doc/build/orm/internals.rst10
-rw-r--r--doc/build/orm/self_referential.rst64
-rw-r--r--doc/build/orm/tutorial.rst23
-rw-r--r--examples/elementtree/adjacency_list.py49
-rw-r--r--examples/elementtree/optimized_al.py56
-rw-r--r--lib/sqlalchemy/orm/query.py335
-rw-r--r--lib/sqlalchemy/orm/util.py15
8 files changed, 275 insertions, 279 deletions
diff --git a/doc/build/conf.py b/doc/build/conf.py
index d4dfd18ce..4ebb93b85 100644
--- a/doc/build/conf.py
+++ b/doc/build/conf.py
@@ -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 = {
diff --git a/doc/build/orm/internals.rst b/doc/build/orm/internals.rst
index e9f1b431e..c9683e145 100644
--- a/doc/build/orm/internals.rst
+++ b/doc/build/orm/internals.rst
@@ -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:
diff --git a/doc/build/orm/self_referential.rst b/doc/build/orm/self_referential.rst
index 4f656e44b..739b6e068 100644
--- a/doc/build/orm/self_referential.rst
+++ b/doc/build/orm/self_referential.rst
@@ -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:
diff --git a/doc/build/orm/tutorial.rst b/doc/build/orm/tutorial.rst
index 85638f622..e77964a7b 100644
--- a/doc/build/orm/tutorial.rst
+++ b/doc/build/orm/tutorial.rst
@@ -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
----------------
diff --git a/examples/elementtree/adjacency_list.py b/examples/elementtree/adjacency_list.py
index 0a5a922d0..cee73bffd 100644
--- a/examples/elementtree/adjacency_list.py
+++ b/examples/elementtree/adjacency_list.py
@@ -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()
)
diff --git a/examples/elementtree/optimized_al.py b/examples/elementtree/optimized_al.py
index 0ba2d1ea4..158b335fd 100644
--- a/examples/elementtree/optimized_al.py
+++ b/examples/elementtree/optimized_al.py
@@ -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()
)
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 5588828eb..6e22a6904 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -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.
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 3558a7c5f..8513374a8 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -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