summaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2021-10-04 22:11:28 +0000
committerGerrit Code Review <gerrit@ci3.zzzcomputing.com>2021-10-04 22:11:28 +0000
commitbb3560851280d338ffb03b72da25488f7db34d22 (patch)
tree015f3ba5d9631b52b83b9fe4f28b01b2de03c2b5 /doc
parentc33a0a9c4732743a58e5e40ae6d8fd52e7865ca8 (diff)
parente7e0757efe042b8343ef44d4f61a33cdbc072ff3 (diff)
downloadsqlalchemy-bb3560851280d338ffb03b72da25488f7db34d22.tar.gz
Merge "disallow adding to identity map that's been discarded"
Diffstat (limited to 'doc')
-rw-r--r--doc/build/changelog/unreleased_14/7128.rst18
-rw-r--r--doc/build/errors.rst79
2 files changed, 97 insertions, 0 deletions
diff --git a/doc/build/changelog/unreleased_14/7128.rst b/doc/build/changelog/unreleased_14/7128.rst
new file mode 100644
index 000000000..85991d21c
--- /dev/null
+++ b/doc/build/changelog/unreleased_14/7128.rst
@@ -0,0 +1,18 @@
+.. change::
+ :tags: bug, orm
+ :tickets: 7128
+
+ Fixed bug where iterating a :class:`.Result` from a :class:`_orm.Session`
+ after that :class:`_orm.Session` were closed would partially attach objects
+ to that session in an essentially invalid state. It now raises an exception
+ with a link to new documentation if an **un-buffered** result is iterated
+ from a :class:`_orm.Session` that was closed or otherwise had the
+ :meth:`_orm.Session.expunge_all` method called after that :class:`.Result`
+ was generated. The "prebuffer_rows" execution option, as is used by the
+ asyncio extension, may be used to produce a :class:`.Result` where the ORM
+ objects are prebuffered, and in this case iterating the result will produce
+ a series of detached objects.
+
+ .. seealso::
+
+ :ref:`error_lkrp` \ No newline at end of file
diff --git a/doc/build/errors.rst b/doc/build/errors.rst
index da5262381..2b163ec26 100644
--- a/doc/build/errors.rst
+++ b/doc/build/errors.rst
@@ -1409,6 +1409,85 @@ items in each case::
Above, the ORM will know that the overlap between ``Parent.c1``,
``Parent.c2`` and ``Child.parent`` is intentional.
+.. _error_lkrp:
+
+Object cannot be converted to 'persistent' state, as this identity map is no longer valid.
+-------------------------------------------------------------------------------------------
+
+.. versionadded:: 1.4.26
+
+This message was added to accommodate for the case where a
+:class:`_result.Result` object that would yield ORM objects is iterated after
+the originating :class:`_orm.Session` has been closed, or otherwise had its
+:meth:`_orm.Session.expunge_all` method called. When a :class:`_orm.Session`
+expunges all objects at once, the internal :term:`identity map` used by that
+:class:`_orm.Session` is replaced with a new one, and the original one
+discarded. An unconsumed and unbuffered :class:`_result.Result` object will
+internally maintain a reference to that now-discarded identity map. Therefore,
+when the :class:`_result.Result` is consumed, the objects that would be yielded
+cannot be associated with that :class:`_orm.Session`. This arrangement is by
+design as it is generally not recommended to iterate an unbuffered
+:class:`_result.Result` object outside of the transactional context in which it
+was created::
+
+ # context manager creates new Session
+ with Session(engine) as session_obj:
+ result = sess.execute(select(User).where(User.id == 7))
+
+ # context manager is closed, so session_obj above is closed, identity
+ # map is replaced
+
+ # iterating the result object can't associate the object with the
+ # Session, raises this error.
+ user = result.first()
+
+The above situation typically will **not** occur when using the ``asyncio``
+ORM extension, as when :class:`.AsyncSession` returns a sync-style
+:class:`_result.Result`, the results have been pre-buffered when the statement
+was executed. This is to allow secondary eager loaders to invoke without needing
+an additional ``await`` call.
+
+To pre-buffer results in the above situation using the regular
+:class:`_orm.Session` in the same way that the ``asyncio`` extension does it,
+the ``prebuffer_rows`` execution option may be used as follows::
+
+ # context manager creates new Session
+ with Session(engine) as session_obj:
+
+ # result internally pre-fetches all objects
+ result = sess.execute(
+ select(User).where(User.id == 7),
+ execution_options={"prebuffer_rows": True}
+ )
+
+ # context manager is closed, so session_obj above is closed, identity
+ # map is replaced
+
+ # pre-buffered objects are returned
+ user = result.first()
+
+ # however they are detached from the session, which has been closed
+ assert inspect(user).detached
+ assert inspect(user).session is None
+
+Above, the selected ORM objects are fully generated within the ``session_obj``
+block, associated with ``session_obj`` and buffered within the
+:class:`_result.Result` object for iteration. Outside the block,
+``session_obj`` is closed and expunges these ORM objects. Iterating the
+:class:`_result.Result` object will yield those ORM objects, however as their
+originating :class:`_orm.Session` has expunged them, they will be delivered in
+the :term:`detached` state.
+
+.. note:: The above reference to a "pre-buffered" vs. "un-buffered"
+ :class:`_result.Result` object refers to the process by which the ORM
+ converts incoming raw database rows from the :term:`DBAPI` into ORM
+ objects. It does not imply whether or not the underyling ``cursor``
+ object itself, which represents pending results from the DBAPI, is itself
+ buffered or unbuffered, as this is essentially a lower layer of buffering.
+ For background on buffering of the ``cursor`` results itself, see the
+ section :ref:`engine_stream_results`.
+
+
AsyncIO Exceptions
==================