]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
modernize contains_eager() docs
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 2 Dec 2020 14:26:18 +0000 (09:26 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 2 Dec 2020 14:49:59 +0000 (09:49 -0500)
Along with other loader options that are likely
to require usage of populate_existing(), make sure
contains_eager() documentation makes it absolutely
clear that already-loaded collections are not overwritten.
consolidate contains_eager() documentation into the narrative docs.

Additionally, remove the "arbitrary statements" section, this is
not a useful case and seems to be left over.

Fixes: #5740
Change-Id: I10e320882990f511eefebcc88cfcc2277e553b50

doc/build/orm/loading_relationships.rst
lib/sqlalchemy/orm/strategy_options.py

index 94bfc6e938d8eace106fed42255786e3f868ac7a..e1ae5e0b188306553d125f39851ba2fd92b846c8 100644 (file)
@@ -169,7 +169,7 @@ to the given filter criteria::
 
 When using limiting criteria, if a particular collection is already loaded
 it won't be refreshed; to ensure the new criteria takes place, apply
-the :meth:`_orm.Query.populate_existing` option::
+the :meth:`_query.Query.populate_existing` option::
 
     session.query(A).options(lazyload(A.bs.and_(B.id > 5))).populate_existing()
 
@@ -1062,10 +1062,9 @@ and additionally establish this as the basis for eager loading of ``User.address
                 options(contains_eager(User.addresses))
 
 
-If the "eager" portion of the statement is "aliased", the ``alias`` keyword
-argument to :func:`~sqlalchemy.orm.contains_eager` may be used to indicate it.
-This is sent as a reference to an :func:`.aliased` or :class:`_expression.Alias`
-construct:
+If the "eager" portion of the statement is "aliased", the path
+should be specified using :meth:`.PropComparator.of_type`, which allows
+the specific :func:`_orm.aliased` construct to be passed:
 
 .. sourcecode:: python+sql
 
@@ -1074,8 +1073,8 @@ construct:
 
     # construct a Query object which expects the "addresses" results
     query = session.query(User).\
-        outerjoin(adalias, User.addresses).\
-        options(contains_eager(User.addresses, alias=adalias))
+        outerjoin(User.addresses.of_type(adalias)).\
+        options(contains_eager(User.addresses.of_type(adalias)))
 
     # get results normally
     r = query.all()
@@ -1092,13 +1091,7 @@ construct:
 
 The path given as the argument to :func:`.contains_eager` needs
 to be a full path from the starting entity. For example if we were loading
-``Users->orders->Order->items->Item``, the string version would look like::
-
-    query(User).options(
-        contains_eager('orders').
-        contains_eager('items'))
-
-Or using the class-bound descriptor::
+``Users->orders->Order->items->Item``, the option would be used as::
 
     query(User).options(
         contains_eager(User.orders).
@@ -1114,64 +1107,43 @@ by writing our SQL to load a subset of elements for collections or
 scalar attributes.
 
 As an example, we can load a ``User`` object and eagerly load only particular
-addresses into its ``.addresses`` collection just by filtering::
+addresses into its ``.addresses`` collection by filtering the joined data,
+routing it using :func:`_orm.contains_eager`, also using
+:meth:`_query.Query.populate_existing` to ensure any already-loaded collections
+are overwritten::
 
-    q = session.query(User).join(User.addresses).\
-                filter(Address.email.like('%ed%')).\
-                options(contains_eager(User.addresses))
+    q = session.query(User).\
+            join(User.addresses).\
+            filter(Address.email_address.like('%@aol.com')).\
+            options(contains_eager(User.addresses)).\
+            populate_existing()
 
 The above query will load only ``User`` objects which contain at
-least ``Address`` object that contains the substring ``'ed'`` in its
+least ``Address`` object that contains the substring ``'aol.com'`` in its
 ``email`` field; the ``User.addresses`` collection will contain **only**
 these ``Address`` entries, and *not* any other ``Address`` entries that are
 in fact associated with the collection.
 
-.. warning::
-
-    Keep in mind that when we load only a subset of objects into a collection,
-    that collection no longer represents what's actually in the database.  If
-    we attempted to add entries to this collection, we might find ourselves
-    conflicting with entries that are already in the database but not locally
-    loaded.
-
-    In addition, the **collection will fully reload normally** once the
-    object or attribute is expired.  This expiration occurs whenever the
-    :meth:`.Session.commit`, :meth:`.Session.rollback` methods are used
-    assuming default session settings, or the :meth:`.Session.expire_all`
-    or :meth:`.Session.expire` methods are used.
-
-    For these reasons, prefer returning separate fields in a tuple rather
-    than artificially altering a collection, when an object plus a custom
-    set of related objects is desired::
-
-        q = session.query(User, Address).join(User.addresses).\
-                    filter(Address.email.like('%ed%'))
-
-
-Advanced Usage with Arbitrary Statements
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-The ``alias`` argument can be more creatively used, in that it can be made
-to represent any set of arbitrary names to match up into a statement.
-Below it is linked to a :func:`_expression.select` which links a set of column objects
-to a string SQL statement::
-
-    # label the columns of the addresses table
-    eager_columns = select(
-        addresses.c.address_id.label('a1'),
-        addresses.c.email_address.label('a2'),
-        addresses.c.user_id.label('a3')
-    )
-
-    # select from a raw SQL statement which uses those label names for the
-    # addresses table.  contains_eager() matches them up.
-    query = session.query(User).\
-        from_statement("select users.*, addresses.address_id as a1, "
-                "addresses.email_address as a2, "
-                "addresses.user_id as a3 "
-                "from users left outer join "
-                "addresses on users.user_id=addresses.user_id").\
-        options(contains_eager(User.addresses, alias=eager_columns))
+.. tip::  In all cases, the SQLAlchemy ORM does **not overwrite already loaded
+   attributes and collections** unless told to do so.   As there is an
+   :term:`identity map` in use, it is often the case that an ORM query is
+   returning objects that were in fact already present and loaded in memory.
+   Therefore, when using :func:`_orm.contains_eager` to populate a collection
+   in an alternate way, it is usually a good idea to use
+   :meth:`_query.Query.populate_existing` as illustrated above so that an
+   already-loaded collection is refreshed with the new data.
+   :meth:`_query.Query.populate_existing` will reset **all** attributes that were
+   already present, including pending changes, so make sure all data is flushed
+   before using it.   Using the :class:`_orm.Session` with its default behavior
+   of :ref:`autoflush <session_flushing>` is sufficient.
+
+.. note::   The customized collection we load using :func:`_orm.contains_eager`
+   is not "sticky"; that is, the next time this collection is loaded, it will
+   be loaded with its usual default contents.   The collection is subject
+   to being reloaded if the object is expired, which occurs whenever the
+   :meth:`.Session.commit`, :meth:`.Session.rollback` methods are used
+   assuming default session settings, or the :meth:`.Session.expire_all`
+   or :meth:`.Session.expire` methods are used.
 
 Creating Custom Load Rules
 --------------------------
index 1795fe6e593a0888ea3fb7de034e9fa204a38252..c5d5b146d42457d6ab8a4155d709810195e5b673 100644 (file)
@@ -1006,40 +1006,18 @@ def contains_eager(loadopt, attr, alias=None):
     ``User`` entity, and the returned ``Order`` objects would have the
     ``Order.user`` attribute pre-populated.
 
-    When making use of aliases with :func:`.contains_eager`, the path
-    should be specified using :meth:`.PropComparator.of_type`::
+    It may also be used for customizing the entries in an eagerly loaded
+    collection; queries will normally want to use the
+    :meth:`_query.Query.populate_existing` method assuming the primary
+    collection of parent objects may already have been loaded::
 
-        user_alias = aliased(User)
-        sess.query(Order).\
-                join((user_alias, Order.user)).\
-                options(contains_eager(Order.user.of_type(user_alias)))
-
-    :meth:`.PropComparator.of_type` is also used to indicate a join
-    against specific subclasses of an inherting mapper, or
-    of a :func:`.with_polymorphic` construct::
-
-        # employees of a particular subtype
-        sess.query(Company).\
-            outerjoin(Company.employees.of_type(Manager)).\
-            options(
-                contains_eager(
-                    Company.employees.of_type(Manager),
-                )
-            )
-
-        # employees of a multiple subtypes
-        wp = with_polymorphic(Employee, [Manager, Engineer])
-        sess.query(Company).\
-            outerjoin(Company.employees.of_type(wp)).\
-            options(
-                contains_eager(
-                    Company.employees.of_type(wp),
-                )
-            )
+        sess.query(User).\
+            join(User.addresses).\
+            filter(Address.email_address.like('%@aol.com')).\
+            options(contains_eager(User.addresses)).\
+            populate_existing()
 
-    The :paramref:`.contains_eager.alias` parameter is used for a similar
-    purpose, however the :meth:`.PropComparator.of_type` approach should work
-    in all cases and is more effective and explicit.
+    See the section :ref:`contains_eager` for complete usage details.
 
     .. seealso::
 
@@ -1690,7 +1668,7 @@ def with_expression(loadopt, key, expression):
 
     .. note:: the target attribute is populated only if the target object
        is **not currently loaded** in the current :class:`_orm.Session`
-       unless the :meth:`_orm.Query.populate_existing` method is used.
+       unless the :meth:`_query.Query.populate_existing` method is used.
        Please refer to :ref:`mapper_querytime_expression` for complete
        usage details.