From: Mike Bayer Date: Wed, 2 Dec 2020 14:26:18 +0000 (-0500) Subject: modernize contains_eager() docs X-Git-Tag: rel_1_3_21~8 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=afa66740328d0bc011304c51b0352b32ca6aade0;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git modernize contains_eager() docs 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 (cherry picked from commit 0d76857ee1022f41baffff4091be765deea4e421) --- diff --git a/doc/build/orm/loading_relationships.rst b/doc/build/orm/loading_relationships.rst index 263d91db2a..bfa1b7c19f 100644 --- a/doc/build/orm/loading_relationships.rst +++ b/doc/build/orm/loading_relationships.rst @@ -1015,10 +1015,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 @@ -1027,8 +1026,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() @@ -1045,13 +1044,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). @@ -1067,64 +1060,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 ` 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 -------------------------- diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index d5404b550f..48e48bebab 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -1030,40 +1030,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:: @@ -1706,7 +1684,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.