]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add more docs for populate_existing(); link with_for_update
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 10 Sep 2020 15:56:34 +0000 (11:56 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 10 Sep 2020 16:15:16 +0000 (12:15 -0400)
The populate_existing() method is actually changing
to be an execution option, however it has almost no
mention in the narrative docs so add docs in terms of the
1.x version first, including that we mention you almost
definitely want to use this method if you are also using
with_for_update().

Fixes: #5572
Fixes: #4774
Change-Id: Ieca916400622c1ffc1ae81204132a02a0983594c

doc/build/faq/sessions.rst
doc/build/orm/session_basics.rst
doc/build/orm/session_state_management.rst
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/session.py

index 835ce301ff321b900510613aefd985873c3e96c2..d9972cb86b58b332201c8b56d2372806a1cd7ec7 100644 (file)
@@ -6,6 +6,7 @@ Sessions / Queries
     :class: faq
     :backlinks: none
 
+.. _faq_session_identity:
 
 I'm re-loading data with my Session but it isn't seeing changes that I committed elsewhere
 ------------------------------------------------------------------------------------------
index 7c0cf1415f8a697b07a2c03ce6225954d10d71b4..7004e3edd1d7b4a6ad514ce3a2c2a037a1dea794 100644 (file)
@@ -298,6 +298,8 @@ via standard methods such as :meth:`_engine.Result.all`,
 
     :ref:`migration_20_toplevel`
 
+
+
 Adding New or Existing Items
 ----------------------------
 
@@ -452,6 +454,76 @@ required after a flush fails, even though the underlying transaction will have
 been rolled back already - this is so that the overall nesting pattern of
 so-called "subtransactions" is consistently maintained.
 
+
+Expiring / Refreshing
+---------------------
+
+An important consideration that will often come up when using the
+:class:`_orm.Session` is that of dealing with the state that is present on
+objects that have been loaded from the database, in terms of keeping them
+synchronized with the current state of the transaction.   The SQLAlchemy
+ORM is based around the concept of an :term:`identity map` such that when
+an object is "loaded" from a SQL query, there will be a unique Python
+object instance maintained corresponding to a particular database identity.
+This means if we emit two separate queries, each for the same row, and get
+a mapped object back, the two queries will have returned the same Python
+object::
+
+  >>> u1 = session.query(User).filter(id=5).first()
+  >>> u2 = session.query(User).filter(id=5).first()
+  >>> u1 is u2
+  True
+
+Following from this, when the ORM gets rows back from a query, it will
+**skip the population of attributes** for an object that's already loaded.
+The design assumption here is to assume a transaction that's perfectly
+isolated, and then to the degree that the transaction isn't isolated, the
+application can take steps on an as-needed basis to refresh objects
+from the database transaction.  The FAQ entry at :ref:`faq_session_identity`
+discusses this concept in more detail.
+
+When an ORM mapped object is loaded into memory, there are three general
+ways to refresh its contents with new data from the current transaction:
+
+* **the expire() method** - the :meth:`_orm.Session.expire` method will
+  erase the contents of selected or all attributes of an object, such that they
+  will be loaded from the database when they are next accessed, e.g. using
+  a :term:`lazy loading` pattern::
+
+    session.expire(u1)
+    u1.some_attribute  # <-- lazy loads from the transaction
+  ..
+
+* **the refresh() method** - closely related is the :meth:`_orm.Session.refresh`
+  method, which does everything the :meth:`_orm.Session.expire` method does
+  but also emits one or more SQL queries immediately to actually refresh
+  the contents of the object::
+
+    session.refresh(u1)  # <-- emits a SQL query
+    u1.some_attribute  # <-- is refreshed from the transaction
+
+  ..
+
+* **the populate_existing() method** - this method is actually on the
+  :class:`_orm.Query` object as :meth:`_orm.Query.populate_existing`
+  and indicates that it should return objects that are unconditionally
+  re-populated from their contents in the database::
+
+    u2 = session.query(User).populate_existing().filter(id=5).first()
+
+  ..
+
+Further discussion on the refresh / expire concept can be found at
+:ref:`session_expire`.
+
+.. seealso::
+
+  :ref:`session_expire`
+
+  :ref:`faq_session_identity`
+
+
+
 .. _bulk_update_delete:
 
 Bulk UPDATE and DELETE
index 1a5168eb6339661412d742446095d1559db40fdb..40e50cda15abcc8f963025b9bcdbae0c8c205cac 100644 (file)
@@ -506,6 +506,12 @@ attributes to be marked as expired::
     # expire only attributes obj1.attr1, obj1.attr2
     session.expire(obj1, ['attr1', 'attr2'])
 
+The :meth:`.Session.expire_all` method allows us to essentially call
+:meth:`.Session.expire` on all objects contained within the :class:`.Session`
+at once::
+
+    session.expire_all()
+
 The :meth:`~.Session.refresh` method has a similar interface, but instead
 of expiring, it emits an immediate SELECT for the object's row immediately::
 
@@ -519,11 +525,14 @@ be that of a column-mapped attribute::
     # reload obj1.attr1, obj1.attr2
     session.refresh(obj1, ['attr1', 'attr2'])
 
-The :meth:`.Session.expire_all` method allows us to essentially call
-:meth:`.Session.expire` on all objects contained within the :class:`.Session`
-at once::
+An alternative method of refreshing which is often more flexible is to
+use the :meth:`_orm.Query.populate_existing` method of :class:`_orm.Query`.
+With this option, all of the ORM objects returned by the :class:`_orm.Query`
+will be refreshed with the contents of what was loaded in the SELECT::
+
+    for user in session.query(User).populate_existing().filter(User.name.in_(['a', 'b', 'c'])):
+        print(user)  # will be refreshed for those columns that came back from the query
 
-    session.expire_all()
 
 What Actually Loads
 ~~~~~~~~~~~~~~~~~~~
@@ -631,6 +640,10 @@ transactions, an understanding of the isolation behavior in effect is essential.
 
     :meth:`.Session.refresh`
 
+    :meth:`_orm.Query.populate_existing` - :class:`_orm.Query` method that refreshes
+    all matching objects in the identity map against the results of a
+    SELECT statement.
+
     :term:`isolation` - glossary explanation of isolation which includes links
     to Wikipedia.
 
index d941d79d2bf0f12ae9be3136cf6d5b0cedbab01b..26a5a89c78ca1eedc9b184317c5f608bc510680c 100644 (file)
@@ -993,6 +993,10 @@ class Query(
             ``populate_existing=True`` option to the
             :meth:`_orm.Query.execution_options` method.
 
+        .. seealso::
+
+            :ref:`session_expire` - in the ORM :class:`_orm.Session`
+            documentation
 
         """
         self.load_options += {"_populate_existing": True}
@@ -1552,19 +1556,30 @@ class Query(
 
         E.g.::
 
-            q = sess.query(User).with_for_update(nowait=True, of=User)
+            q = sess.query(User).populate_existing().with_for_update(nowait=True, of=User)
 
         The above query on a PostgreSQL backend will render like::
 
             SELECT users.id AS users_id FROM users FOR UPDATE OF users NOWAIT
 
+        .. note::  It is generally a good idea to combine the use of the
+           :meth:`_orm.Query.populate_existing` method when using the
+           :meth:`_orm.Query.with_for_update` method.   The purpose of
+           :meth:`_orm.Query.populate_existing` is to force all the data read
+           from the SELECT to be populated into the ORM objects returned,
+           even if these objects are already in the :term:`identity map`.
+
         .. seealso::
 
             :meth:`_expression.GenerativeSelect.with_for_update`
             - Core level method with
             full argument and behavioral description.
 
-        """
+            :meth:`_orm.Query.populate_existing` - overwrites attributes of
+            objects already loaded in the identity map.
+
+        """  # noqa: E501
+
         self._for_update_arg = ForUpdateArg(
             read=read,
             nowait=nowait,
index e844836bafbcb4f1edd240d745fad9e8b63842a5..0c012d7f3f231dddf99eb93845bd883bd1913f47 100644 (file)
@@ -2137,6 +2137,8 @@ class Session(_SessionClassMethods):
 
             :meth:`.Session.expire_all`
 
+            :meth:`_orm.Query.populate_existing`
+
         """
         try:
             state = attributes.instance_state(instance)
@@ -2201,6 +2203,8 @@ class Session(_SessionClassMethods):
 
             :meth:`.Session.refresh`
 
+            :meth:`_orm.Query.populate_existing`
+
         """
         for state in self.identity_map.all_states():
             state._expire(state.dict, self.identity_map._modified)
@@ -2239,6 +2243,8 @@ class Session(_SessionClassMethods):
 
             :meth:`.Session.refresh`
 
+            :meth:`_orm.Query.populate_existing`
+
         """
         try:
             state = attributes.instance_state(instance)