diff options
| -rw-r--r-- | doc/build/conf.py | 2 | ||||
| -rw-r--r-- | doc/build/orm/internals.rst | 10 | ||||
| -rw-r--r-- | doc/build/orm/self_referential.rst | 64 | ||||
| -rw-r--r-- | doc/build/orm/tutorial.rst | 23 | ||||
| -rw-r--r-- | examples/elementtree/adjacency_list.py | 49 | ||||
| -rw-r--r-- | examples/elementtree/optimized_al.py | 56 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 335 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 15 |
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 |
