diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-09-02 17:55:26 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-09-02 17:55:26 -0400 |
| commit | 606135dd989f3f9b057559128a185e9f7e5364f0 (patch) | |
| tree | 22653edc53d9936b90cbfe1eb75e44d337ca0a88 /doc | |
| parent | 6d0c0994e9a08cf5d149af0314970d5f6e25b159 (diff) | |
| parent | 108c60f460c723a0f48c47597928d938a3b0a42d (diff) | |
| download | sqlalchemy-606135dd989f3f9b057559128a185e9f7e5364f0.tar.gz | |
Merge branch 'ticket_2677'
Diffstat (limited to 'doc')
| -rw-r--r-- | doc/build/changelog/changelog_11.rst | 44 | ||||
| -rw-r--r-- | doc/build/changelog/migration_11.rst | 90 | ||||
| -rw-r--r-- | doc/build/core/events.rst | 4 | ||||
| -rw-r--r-- | doc/build/glossary.rst | 23 | ||||
| -rw-r--r-- | doc/build/orm/events.rst | 10 | ||||
| -rw-r--r-- | doc/build/orm/examples.rst | 2 | ||||
| -rw-r--r-- | doc/build/orm/session.rst | 1 | ||||
| -rw-r--r-- | doc/build/orm/session_events.rst | 433 | ||||
| -rw-r--r-- | doc/build/orm/session_state_management.rst | 126 | ||||
| -rw-r--r-- | doc/build/orm/session_transaction.rst | 7 |
10 files changed, 691 insertions, 49 deletions
diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index 4fff4ab64..27dc8fd46 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -22,6 +22,50 @@ :version: 1.1.0b1 .. change:: + :tags: feature, orm + :tickets: 2677 + + The :class:`.SessionEvents` suite now includes events to allow + unambiguous tracking of all object lifecycle state transitions + in terms of the :class:`.Session` itself, e.g. pending, + transient, persistent, detached. The state of the object + within each event is also defined. + + .. seealso:: + + :ref:`change_2677` + + .. change:: + :tags: feature, orm + :tickets: 2677 + + Added a new session lifecycle state :term:`deleted`. This new state + represents an object that has been deleted from the :term:`persistent` + state and will move to the :term:`detached` state once the transaction + is committed. This resolves the long-standing issue that objects + which were deleted existed in a gray area between persistent and + detached. The :attr:`.InstanceState.persistent` accessor will + **no longer** report on a deleted object as persistent; the + :attr:`.InstanceState.deleted` accessor will instead be True for + these objects, until they become detached. + + .. seealso:: + + :ref:`change_2677` + + .. change:: + :tags: change, orm + :tickets: 2677 + + The :paramref:`.Session.weak_identity_map` parameter is deprecated. + See the new recipe at :ref:`session_referencing_behavior` for + an event-based approach to maintaining strong identity map behavior. + + .. seealso:: + + :ref:`change_2677` + + .. change:: :tags: bug, sql :tickets: 2919 diff --git a/doc/build/changelog/migration_11.rst b/doc/build/changelog/migration_11.rst index 412f42d27..ff26a00bd 100644 --- a/doc/build/changelog/migration_11.rst +++ b/doc/build/changelog/migration_11.rst @@ -16,7 +16,7 @@ What's New in SQLAlchemy 1.1? some issues may be moved to later milestones in order to allow for a timely release. - Document last updated: August 26, 2015 + Document last updated: September 2, 2015 Introduction ============ @@ -66,6 +66,94 @@ as it relies on deprecated features of setuptools. New Features and Improvements - ORM =================================== +.. _change_2677: + +New Session lifecycle events +---------------------------- + +The :class:`.Session` has long supported events that allow some degree +of tracking of state changes to objects, including +:meth:`.SessionEvents.before_attach`, :meth:`.SessionEvents.after_attach`, +and :meth:`.SessionEvents.before_flush`. The Session documentation also +documents major object states at :ref:`session_object_states`. However, +there has never been system of tracking objects specifically as they +pass through these transitions. Additionally, the status of "deleted" objects +has historically been murky as the objects act somewhere between +the "persistent" and "detached" states. + +To clean up this area and allow the realm of session state transition +to be fully transparent, a new series of events have been added that +are intended to cover every possible way that an object might transition +between states, and additionally the "deleted" status has been given +its own official state name within the realm of session object states. + +New State Transition Events +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Transitions between all states of an object such as :term:`persistent`, +:term:`pending` and others can now be intercepted in terms of a +session-level event intended to cover a specific transition. +Transitions as objects move into a :class:`.Session`, move out of a +:class:`.Session`, and even all the transitions which occur when the +transaction is rolled back using :meth:`.Session.rollback` +are explicitly present in the interface of :class:`.SessionEvents`. + +In total, there are **ten new events**. A summary of these events is in a +newly written documentation section :ref:`session_lifecycle_events`. + + +New Object State "deleted" is added, deleted objects no longer "persistent" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :term:`persistent` state of an object in the :class:`.Session` has +always been documented as an object that has a valid database identity; +however in the case of objects that were deleted within a flush, they +have always been in a grey area where they are not really "detached" +from the :class:`.Session` yet, because they can still be restored +within a rollback, but are not really "persistent" because their database +identity has been deleted and they aren't present in the identity map. + +To resolve this grey area given the new events, a new object state +:term:`deleted` is introduced. This state exists between the "persistent" and +"detached" states. An object that is marked for deletion via +:meth:`.Session.delete` remains in the "persistent" state until a flush +proceeds; at that point, it is removed from the identity map, moves +to the "deleted" state, and the :meth:`.SessionEvents.persistent_to_deleted` +hook is invoked. If the :class:`.Session` object's transaction is rolled +back, the object is restored as persistent; the +:meth:`.SessionEvents.deleted_to_persistent` transition is called. Otherwise +if the :class:`.Session` object's transaction is committed, +the :meth:`.SessionEvents.deleted_to_detached` transition is invoked. + +Additionally, the :attr:`.InstanceState.persistent` accessor **no longer returns +True** for an object that is in the new "deleted" state; instead, the +:attr:`.InstanceState.deleted` accessor has been enhanced to reliably +report on this new state. When the object is detached, the :attr:`.InstanceState.deleted` +returns False and the :attr:`.InstanceState.detached` accessor is True +instead. To determine if an object was deleted either in the current +transaction or in a previous transaction, use the +:attr:`.InstanceState.was_deleted` accessor. + +Strong Identity Map is Deprecated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +One of the inspirations for the new series of transition events was to enable +leak-proof tracking of objects as they move in and out of the identity map, +so that a "strong reference" may be maintained mirroring the object +moving in and out of this map. With this new capability, there is no longer +any need for the :paramref:`.Session.weak_identity_map` parameter and the +corresponding :class:`.StrongIdentityMap` object. This option has remained +in SQLAlchemy for many years as the "strong-referencing" behavior used to be +the only behavior available, and many applications were written to assume +this behavior. It has long been recommended that strong-reference tracking +of objects not be an intrinsic job of the :class:`.Session` and instead +be an application-level construct built as needed by the application; the +new event model allows even the exact behavior of the strong identity map +to be replicated. See :ref:`session_referencing_behavior` for a new +recipe illustrating how to replace the strong identity map. + +:ticket:`2677` + .. _change_3499: Changes regarding "unhashable" types diff --git a/doc/build/core/events.rst b/doc/build/core/events.rst index d19b910b1..451cb9460 100644 --- a/doc/build/core/events.rst +++ b/doc/build/core/events.rst @@ -11,10 +11,6 @@ ORM events are described in :ref:`orm_event_toplevel`. .. autoclass:: sqlalchemy.event.base.Events :members: -.. versionadded:: 0.7 - The event system supersedes the previous system of "extension", "listener", - and "proxy" classes. - Connection Pool Events ----------------------- diff --git a/doc/build/glossary.rst b/doc/build/glossary.rst index c0ecee84b..9c1395f14 100644 --- a/doc/build/glossary.rst +++ b/doc/build/glossary.rst @@ -1019,7 +1019,7 @@ Glossary http://en.wikipedia.org/wiki/Unique_key#Defining_unique_keys transient - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a transient object is a new object that doesn't have any database identity and has not been associated with a session yet. When the @@ -1031,7 +1031,7 @@ Glossary :ref:`session_object_states` pending - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a pending object is a new object that doesn't have any database identity, but has been recently associated with a session. When @@ -1042,8 +1042,23 @@ Glossary :ref:`session_object_states` + deleted + This describes one of the major object states which + an object can have within a :term:`session`; a deleted object + is an object that was formerly persistent and has had a + DELETE statement emitted to the database within a flush + to delete its row. The object will move to the :term:`detached` + state once the session's transaction is committed; alternatively, + if the session's transaction is rolled back, the DELETE is + reverted and the object moves back to the :term:`persistent` + state. + + .. seealso:: + + :ref:`session_object_states` + persistent - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a persistent object is an object that has a database identity (i.e. a primary key) and is currently associated with a session. Any object @@ -1058,7 +1073,7 @@ Glossary :ref:`session_object_states` detached - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a detached object is an object that has a database identity (i.e. a primary key) but is not associated with any session. An object that diff --git a/doc/build/orm/events.rst b/doc/build/orm/events.rst index e9673bed0..470a9386b 100644 --- a/doc/build/orm/events.rst +++ b/doc/build/orm/events.rst @@ -5,12 +5,10 @@ ORM Events The ORM includes a wide variety of hooks available for subscription. -.. versionadded:: 0.7 - The event supersedes the previous system of "extension" classes. - -For an introduction to the event API, see :ref:`event_toplevel`. Non-ORM events -such as those regarding connections and low-level statement execution are described in -:ref:`core_event_toplevel`. +For an introduction to the most commonly used ORM events, see the section +:ref:`session_events_toplevel`. The event system in general is discussed +at :ref:`event_toplevel`. Non-ORM events such as those regarding connections +and low-level statement execution are described in :ref:`core_event_toplevel`. Attribute Events ---------------- diff --git a/doc/build/orm/examples.rst b/doc/build/orm/examples.rst index 4db7c00dc..25d243022 100644 --- a/doc/build/orm/examples.rst +++ b/doc/build/orm/examples.rst @@ -93,6 +93,8 @@ Versioning with a History Table .. automodule:: examples.versioned_history +.. _examples_versioned_rows: + Versioning using Temporal Rows ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/build/orm/session.rst b/doc/build/orm/session.rst index 624ee9f75..79ea70137 100644 --- a/doc/build/orm/session.rst +++ b/doc/build/orm/session.rst @@ -20,6 +20,7 @@ configured, the primary usage interface for persistence operations is the session_transaction persistence_techniques contextual + session_events session_api diff --git a/doc/build/orm/session_events.rst b/doc/build/orm/session_events.rst new file mode 100644 index 000000000..dc7ad39e0 --- /dev/null +++ b/doc/build/orm/session_events.rst @@ -0,0 +1,433 @@ +.. _session_events_toplevel: + +Tracking Object and Session Changes with Events +=============================================== + +SQLAlchemy features an extensive :ref:`Event Listening <event_toplevel>` +system used throughout the Core and ORM. Within the ORM, there are a +wide variety of event listener hooks, which are documented at an API +level at :ref:`orm_event_toplevel`. This collection of events has +grown over the years to include lots of very useful new events as well +as some older events that aren't as relevant as they once were. This +section will attempt to introduce the major event hooks and when they +might be used. + +.. _session_persistence_events: + +Persistence Events +------------------ + +Probably the most widely used series of events are the "persistence" events, +which correspond to the :ref:`flush process<session_flushing>`. +The flush is where all the decisions are made about pending changes to +objects and are then emitted out to the database in the form of INSERT, +UPDATE, and DELETE staetments. + +``before_flush()`` +^^^^^^^^^^^^^^^^^^ + +The :meth:`.SessionEvents.before_flush` hook is by far the most generally +useful event to use when an application wants to ensure that +additional persistence changes to the database are made when a flush proceeds. +Use :meth:`.SessionEvents.before_flush` in order to operate +upon objects to validate their state as well as to compose additional objects +and references before they are persisted. Within this event, +it is **safe to manipulate the Session's state**, that is, new objects +can be attached to it, objects can be deleted, and indivual attributes +on objects can be changed freely, and these changes will be pulled into +the flush process when the event hook completes. + +The typical :meth:`.SessionEvents.before_flush` hook will be tasked with +scanning the collections :attr:`.Session.new`, :attr:`.Session.dirty` and +:attr:`.Session.deleted` in order to look for objects +where something will be happening. + +For illustrations of :meth:`.SessionEvents.before_flush, see +examples such as :ref:`examples_versioned_history` and +:ref:`examples_versioned_rows`. + +``after_flush()`` +^^^^^^^^^^^^^^^^^ + +The :meth:`.SessionEvents.after_flush` hook is called after the SQL has been +emitted for a flush process, but **before* the state of the objects that +were flushed has been altered. That is, you can still inspect +the :attr:`.Session.new`, :attr:`.Session.dirty` and +:attr:`.Session.deleted` collections to see what was just flushed, and +you can also use history tracking features like the ones provided +by :class:`.AttributeState` to see what changes were just persisted. +In the :meth:`.SessionEvents.after_flush` event, additional SQL can be emitted +to the database based on what's observed to have changed. + +``after_flush_postexec()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`.SessionEvents.after_flush_postexec` is called soon after +:meth:`.SessionEvents.after_flush`, but is invoked **after** the state of +the objects has been modified to account for the flush that just took place. +The :attr:`.Session.new`, :attr:`.Session.dirty` and +:attr:`.Session.deleted` collections are normally completely empty here. +Use :meth:`.SessionEvents.after_flush_postexec` to inspect the identity map +for finalized objects and possibly emit additional SQL. In this hook, +there is the ability to make new changes on objects, which means the +:class:`.Session` will again go into a "dirty" state; the mechanics of the +:class:`.Session` here will cause it to flush **again** if new changes +are detected in this hook. A counter ensures that an endless loop in this +regard is stopped after 100 iterations, in the case that an +:meth:`.SessionEvents.after_flush_postexec` +hook continually adds new state to be flushed each time it is called. + +.. _session_persistence_mapper: + +Mapper-level Events +^^^^^^^^^^^^^^^^^^^ + +In addition to the flush-level hooks, there is also a suite of hooks +that are more fine-grained, in that they are called on a per-object +basis and are broken out based on INSERT, UPDATE or DELETE. These +are the mapper persistence hooks, and they too are very popular, +however these events need to be approached more cautiously, as they +proceed within the context of the flush process that is already +ongoing; many operations are not safe to proceed here. + +The events are: + +* :meth:`.MapperEvents.before_insert` +* :meth:`.MapperEvents.after_insert` +* :meth:`.MapperEvents.before_update` +* :meth:`.MapperEvents.after_update` +* :meth:`.MapperEvents.before_delete` +* :meth:`.MapperEvents.after_delete` + +Each event is passed the :class:`.Mapper`, +the mapped object itself, and the :class:`.Connection` which is being +used to emit an INSERT, UPDATE or DELETE statement. The appeal of these +events is clear, in that if an application wants to tie some activity to +when a specific type of object is persisted with an INSERT, the hook is +very specific; unlike the :meth:`.SessionEvents.before_flush` event, +there's no need to search through collections like :attr:`.Session.new` +in order to find targets. However, the flush plan which +represents the full list of every single INSERT, UPDATE, DELETE statement +to be emitted has *already been decided* when these events are called, +and no changes may be made at this stage. Therefore the only changes that are +even possible to the given objects are upon attributes **local** to the +object's row. Any other change to the object or other objects will +impact the state of the :class:`.Session`, which will fail to function +properly. + +Operations that are not supported within these mapper-level persistence +events include: + +* :meth:`.Session.add` +* :meth:`.Session.delete` +* Mapped collection append, add, remove, delete, discard, etc. +* Mapped relationship attribute set/del events, + i.e. ``someobject.related = someotherobject`` + +The reason the :class:`.Connection` is passed is that it is encouraged that +**simple SQL operations take place here**, directly on the :class:`.Connection`, +such as incrementing counters or inserting extra rows within log tables. +When dealing with the :class:`.Connection`, it is expected that Core-level +SQL operations will be used; e.g. those described in :ref:`sqlexpression_toplevel`. + +There are also many per-object operations that don't need to be handled +within a flush event at all. The most common alternative is to simply +establish additional state along with an object inside its ``__init__()`` +method, such as creating additional objects that are to be associated with +the new object. Using validators as described in :ref:`simple_validators` is +another approach; these functions can intercept changes to attributes and +establish additional state changes on the target object in response to the +attribute change. With both of these approaches, the object is in +the correct state before it ever gets to the flush step. + +.. _session_lifecycle_events: + +Object Lifecycle Events +----------------------- + +Another use case for events is to track the lifecycle of objects. This +refers to the states first introduced at :ref:`session_object_states`. + +.. versionadded:: 1.1 added a system of events that intercept all possible + state transitions of an object within the :class:`.Session`. + +All the states above can be tracked fully with events. Each event +represents a distinct state transition, meaning, the starting state +and the destination state are both part of what are tracked. With the +exception of the initial transient event, all the events are in terms of +the :class:`.Session` object or class, meaning they can be associated either +with a specific :class:`.Session` object:: + + from sqlalchemy import event + from sqlalchemy.orm import Session + + session = Session() + + @event.listens_for(session, 'transient_to_pending') + def object_is_pending(session, obj): + print("new pending: %s" % obj) + +Or with the :class:`.Session` class itself, as well as with a specific +:class:`.sessionmaker`, which is likely the most useful form:: + + from sqlalchemy import event + from sqlalchemy.orm import sessionmaker + + maker = sessionmaker() + + @event.listens_for(maker, 'transient_to_pending') + def object_is_pending(session, obj): + print("new pending: %s" % obj) + +The listeners can of course be stacked on top of one function, as is +likely to be common. For example, to track all objects that are +entering the persistent state:: + + @event.listens_for(maker, "pending_to_persistent") + @event.listens_for(maker, "deleted_to_persistent") + @event.listens_for(maker, "detached_to_persistent") + @event.listens_for(maker, "loaded_as_persistent") + def detect_all_persistent(session, instance): + print("object is now persistent: %s" % instance) + +Transient +^^^^^^^^^ + +All mapped objects when first constructed start out as :term:`transient`. +In this state, the object exists alone and doesn't have an association with +any :class:`.Session`. For this initial state, there's no specific +"transition" event since there is no :class:`.Session`, however if one +wanted to intercept when any transient object is created, the +:meth:`.InstanceEvents.init` method is probably the best event. This +event is applied to a specific class or superclass. For example, to +intercept all new objects for a particular declarative base:: + + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy import event + + Base = declarative_base() + + @event.listens_for(Base, "init", propagate=True) + def intercept_init(instance, args, kwargs): + print("new transient: %s" % instance) + + +Transient to Pending +^^^^^^^^^^^^^^^^^^^^ + +The transient object becomes :term:`pending` when it is first associated +with a :class:`.Session` via the :meth:`.Session.add` or :meth:`.Session.add_all` +method. An object may also become part of a :class:`.Session` as a result +of a :ref:`"cascade" <unitofwork_cascades>` from a referencing object that was +explicitly added. The transient to pending transition is detectable using +the :meth:`.SessionEvents.transient_to_pending` event:: + + @event.listens_for(sessionmaker, "transient_to_pending") + def intercept_transient_to_pending(session, object_): + print("transient to pending: %s" % object_) + + +Pending to Persistent +^^^^^^^^^^^^^^^^^^^^^ + +The :term:`pending` object becomes :term:`persistent` when a flush +proceeds and an INSERT statement takes place for the instance. The object +now has an identity key. Track pending to persistent with the +:meth:`.SessionEvents.pending_to_persistent` event:: + + @event.listens_for(sessionmaker, "pending_to_persistent") + def intercept_pending_to_persistent(session, object_): + print("pending to persistent: %s" % object_) + +Pending to Transient +^^^^^^^^^^^^^^^^^^^^^^^ + +The :term:`pending` object can revert back to :term:`transient` if the +:meth:`.Session.rollback` method is called before the pending object +has been flushed, or if the :meth:`.Session.expunge` method is called +for the object before it is flushed. Track pending to transient with the +:meth:`.SessionEvents.pending_to_transient` event:: + + @event.listens_for(sessionmaker, "pending_to_transient") + def intercept_pending_to_transient(session, object_): + print("transient to pending: %s" % object_) + +Loaded as Persistent +^^^^^^^^^^^^^^^^^^^^^^^ + +Objects can appear in the :class:`.Session` directly in the :term:`persistent` +state when they are loaded from the database. Tracking this state transition +is synonymous with tracking objects as they are loaded, and is synonomous +with using the :meth:`.InstanceEvents.load` instance-level event. However, the +:meth:`.SessionEvents.loaded_as_persistent` event is provided as a +session-centric hook for intercepting objects as they enter the persistent +state via this particular avenue:: + + @event.listens_for(sessionmaker, "loaded_as_persistent") + def intercept_loaded_as_persistent(session, object_): + print("object loaded into persistent state: %s" % object_) + + +Persistent to Transient +^^^^^^^^^^^^^^^^^^^^^^^ + +The persistent object can revert to the transient state if the +:meth:`.Session.rollback` method is called for a transaction where the +object was first added as pending. In the case of the ROLLBACK, the +INSERT statement that made this object persistent is rolled back, and +the object is evicted from the :class:`.Session` to again become transient. +Track objects that were reverted to transient from +persistent using the :meth:`.SessionEvents.persistent_to_transient` +event hook:: + + @event.listens_for(sessionmaker, "persistent_to_transient") + def intercept_persistent_to_transient(session, object_): + print("persistent to transient: %s" % object_) + +Persistent to Deleted +^^^^^^^^^^^^^^^^^^^^^ + +The persistent object enters the :term:`deleted` state when an object +marked for deletion is deleted from the database within the flush +process. Note that this is **not the same** as when the :meth:`.Session.delete` +method is called for a target object. The :meth:`.Session.delete` +method only **marks** the object for deletion; the actual DELETE statement +is not emitted until the flush proceeds. It is subsequent to the flush +that the "deleted" state is present for the target object. + +Within the "deleted" state, the object is only marginally associated +with the :class:`.Session`. It is not present in the identity map +nor is it present in the :attr:`.Session.deleted` collection that refers +to when it was pending for deletion. + +From the "deleted" state, the object can go either to the detached state +when the transaction is committed, or back to the persistent state +if the transaction is instead rolled back. + +Track the persistent to deleted transition with +:meth:`.SessionEvents.persistent_to_deleted`:: + + @event.listens_for(sessionmaker, "persistent_to_deleted") + def intercept_persistent_to_deleted(session, object_): + print("object was DELETEd, is now in deleted state: %s" % object_) + + +Deleted to Detached +^^^^^^^^^^^^^^^^^^^^ + +The deleted object becomes :term:`detached` when the session's transaction +is committed. After the :meth:`.Session.commit` method is called, the +database transaction is final and the :class:`.Session` now fully discards +the deleted object and removes all associations to it. Track +the deleted to detached transition using :meth:`.SessionEvents.deleted_to_detached`:: + + @event.listens_for(sessionmaker, "deleted_to_detached") + def intercept_deleted_to_detached(session, object_): + print("deleted to detached: %s" % object_) + + +.. note:: + + While the object is in the deleted state, the :attr:`.InstanceState.deleted` + attribute, accessible using ``inspect(object).deleted``, returns True. However + when the object is detached, :attr:`.InstanceState.deleted` will again + return False. To detect that an object was deleted, regardless of whether + or not it is detached, use the :attr:`.InstanceState.was_deleted` + accessor. + + +Persistent to Detached +^^^^^^^^^^^^^^^^^^^^^^^ + +The persistent object becomes :term:`detached` when the object is de-associated +with the :class:`.Session`, via the :meth:`.Session.expunge`, +:meth:`.Session.expunge_all`, or :meth:`.Session.close` methods. + +.. note:: + + An object may also become **implicitly detached** if its owning + :class:`.Session` is dereferenced by the application and discarded due to + garbage collection. In this case, **no event is emitted**. + +Track objects as they move from persistent to detached using the +:meth:`.SessionEvents.persistent_to_detached` event:: + + @event.listens_for(sessionmaker, "persistent_to_detached") + def intecept_persistent_to_detached(session, object_): + print("object became detached: %s" % object_) + +Detached to Persistent +^^^^^^^^^^^^^^^^^^^^^^^ + +The detached object becomes persistent when it is re-associated with a +session using the :meth:`.Session.add` or equivalent method. Track +objects moving back to persistent from detached using the +:meth:`.SessionEvents.detached_to_persistent` event:: + + @event.listens_for(sessionmaker, "detached_to_persistent") + def intecept_detached_to_persistent(session, object_): + print("object became persistent again: %s" % object_) + + +Deleted to Persistent +^^^^^^^^^^^^^^^^^^^^^^^ + +The :term:`deleted` object can be reverted to the :term:`persistent` +state when the transaction in which it was DELETEd was rolled back +using the :meth:`.Session.rollback` method. Track deleted objects +moving back to the persistent state using the +:meth:`.SessionEvents.deleted_to_persistent` event:: + + @event.listens_for(sessionmaker, "transient_to_pending") + def intercept_transient_to_pending(session, object_): + print("transient to pending: %s" % object_) + +.. _session_transaction_events: + +Transaction Events +------------------ + +Transaction events allow an application to be notifed when transaction +boundaries occur at the :class:`.Session` level as well as when the +:class:`.Session` changes the transactional state on :class:`.Connection` +objects. + +* :meth:`.SessionEvents.after_transaction_create`, + :meth:`.SessionEvents.after_transaction_end` - these events track the + logical transaction scopes of the :class:`.Session` in a way that is + not specific to individual database connections. These events are + intended to help with integration of transaction-tracking systems such as + ``zope.sqlalchemy``. Use these + events when the application needs to align some external scope with the + transactional scope of the :class:`.Session`. These hooks mirror + the "nested" transactional behavior of the :class:`.Session`, in that they + track logical "subtransactions" as well as "nested" (e.g. SAVEPOINT) + transactions. + +* :meth:`.SessionEvents.before_commit`, :meth:`.SessionEvents.after_commit`, + :meth:`.SessionEvents.after_begin`, + :meth:`.SessionEvents.after_rollback`, :meth:`.SessionEvents.after_soft_rollback` - + These events allow tracking of transaction events from the perspective + of database connections. :meth:`.SessionEvents.after_begin` in particular + is a per-connection event; a :class:`.Session` that maintains more than + one connection will emit this event for each connection individually + as those connections become used within the current transaction. + The rollback and commit events then refer to when the DBAPI connections + themselves have received rollback or commit instructions directly. + +Attribute Change Events +----------------------- + +The attribute change events allow interception of when specific attributes +on an object are modified. These events include :meth:`.AttributeEvents.set`, +:meth:`.AttributeEvents.append`, and :meth:`.AttributeEvents.remove`. These +events are extremely useful, particularly for per-object validation operations; +however, it is often much more convenient to use a "validator" hook, which +uses these hooks behind the scenes; see :ref:`simple_validators` for +background on this. The attribute events are also behind the mechanics +of backreferences. An example illustrating use of attribute events +is in :ref:`examples_instrumentation`. + + + + diff --git a/doc/build/orm/session_state_management.rst b/doc/build/orm/session_state_management.rst index 1a8bb3dee..090bf7674 100644 --- a/doc/build/orm/session_state_management.rst +++ b/doc/build/orm/session_state_management.rst @@ -23,16 +23,28 @@ It's helpful to know the states which an instance can have within a session: existing instances (or moving persistent instances from other sessions into your local session). -* **Detached** - an instance which has a record in the database, but is not in - any session. There's nothing wrong with this, and you can use objects - normally when they're detached, **except** they will not be able to issue - any SQL in order to load collections or attributes which are not yet loaded, - or were marked as "expired". - -Knowing these states is important, since the -:class:`.Session` tries to be strict about ambiguous -operations (such as trying to save the same object to two different sessions -at the same time). +* **Deleted** - An instance which has been deleted within a flush, but + the transaction has not yet completed. Objects in this state are essentially + in the opposite of "pending" state; when the session's transaction is committed, + the object will move to the detached state. Alternatively, when + the session's transaction is rolled back, a deleted object moves + *back* to the persistent state. + + .. versionchanged:: 1.1 The 'deleted' state is a newly added session + object state distinct from the 'persistent' state. + +* **Detached** - an instance which corresponds, or previously corresponded, + to a record in the database, but is not currently in any session. + The detached object will contain a database identity marker, however + because it is not associated with a session, it is unknown whether or not + this database identity actually exists in a target database. Detached + objects are safe to use normally, except that they have no ability to + load unloaded attributes or attributes that were previously marked + as "expired". + +For a deeper dive into all possible state transitions, see the +section :ref:`session_lifecycle_events` which describes each transition +as well as how to programmatically track each one. Getting the Current State of an Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -53,8 +65,12 @@ the :func:`.inspect` system:: :attr:`.InstanceState.persistent` + :attr:`.InstanceState.deleted` + :attr:`.InstanceState.detached` +.. _session_attributes: + Session Attributes ------------------ @@ -91,7 +107,13 @@ all objects which have had changes since they were last loaded or saved (i.e. (Documentation: :attr:`.Session.new`, :attr:`.Session.dirty`, :attr:`.Session.deleted`, :attr:`.Session.identity_map`). -Note that objects within the session are *weakly referenced*. This + +.. _session_referencing_behavior: + +Session Referencing Behavior +---------------------------- + +Objects within the session are *weakly referenced*. This means that when they are dereferenced in the outside application, they fall out of scope from within the :class:`~sqlalchemy.orm.session.Session` as well and are subject to garbage collection by the Python interpreter. The @@ -100,29 +122,65 @@ as deleted, or persistent objects which have pending changes on them. After a full flush, these collections are all empty, and all objects are again weakly referenced. -.. note:: - - To disable the weak referencing behavior and force all objects - within the session to remain until explicitly expunged, configure - :class:`.sessionmaker` with the ``weak_identity_map=False`` - setting. However note that this option is **deprecated**; - it is present only to allow compatibility with older - applications, typically those that were made back before SQLAlchemy - had the ability to effectively weak-reference all objects. - It is recommended that strong references to objects - be maintained by the calling application externally to the - :class:`.Session` itself, to the extent that is required by the application. - This eliminates the - :class:`.Session` as a possible source of unbounded memory growth in the case - where large numbers of objects are being loaded and/or persisted. - - Simple examples of externally managed strong-referencing behavior - include loading objects into a local dictionary keyed to their primary key, - or into lists or sets for the span of time that they need to remain referenced. - These collections can be associated with a :class:`.Session`, if desired, - by placing them into the :attr:`.Session.info` dictionary. Events such - as the :meth:`.SessionEvents.after_attach` event may also be of use for - intercepting objects as they are associated with a :class:`.Session`. +To cause objects in the :class:`.Session` to remain strongly +referenced, usually a simple approach is all that's needed. Examples +of externally managed strong-referencing behavior include loading +objects into a local dictionary keyed to their primary key, or into +lists or sets for the span of time that they need to remain +referenced. These collections can be associated with a +:class:`.Session`, if desired, by placing them into the +:attr:`.Session.info` dictionary. + +An event based approach is also feasable. A simple recipe that provides +"strong referencing" behavior for all objects as they remain within +the :term:`persistent` state is as follows:: + + from sqlalchemy import event + + def strong_reference_session(session): + @event.listens_for(session, "pending_to_persistent") + @event.listens_for(session, "deleted_to_persistent") + @event.listens_for(session, "detached_to_persistent") + @event.listens_for(session, "loaded_as_persistent") + def strong_ref_object(sess, instance): + if 'refs' not in sess.info: + sess.info['refs'] = refs = set() + else: + refs = sess.info['refs'] + + refs.add(instance) + + + @event.listens_for(session, "persistent_to_detached") + @event.listens_for(session, "persistent_to_deleted") + @event.listens_for(session, "persistent_to_transient") + def deref_object(sess, instance): + sess.info['refs'].discard(instance) + +Above, we intercept the :meth:`.SessionEvents.pending_to_persistent`, +:meth:`.SessionEvents.detached_to_persistent`, +:meth:`.SessionEvents.deleted_to_persistent` and +:meth:`.SessionEvents.loaded_as_persistent` event hooks in order to intercept +objects as they enter the :term:`persistent` transition, and the +:meth:`.SessionEvents.persistent_to_detached` and +:meth:`.SessionEvents.persistent_to_deleted` hooks to intercept +objects as they leave the persistent state. + +The above function may be called for any :class:`.Session` in order to +provide strong-referencing behavior on a per-:class:`.Session` basis:: + + from sqlalchemy.orm import Session + + my_session = Session() + strong_reference_session(my_session) + +It may also be called for any :class:`.sessionmaker`:: + + from sqlalchemy.orm import sessionmaker + + maker = sessionmaker() + strong_reference_session(maker) + .. _unitofwork_merging: diff --git a/doc/build/orm/session_transaction.rst b/doc/build/orm/session_transaction.rst index bca3e944f..e197eb715 100644 --- a/doc/build/orm/session_transaction.rst +++ b/doc/build/orm/session_transaction.rst @@ -414,6 +414,12 @@ on the target connection, a warning is emitted:: :paramref:`.Session.connection.execution_options` parameter to :meth:`.Session.connection`. +Tracking Transaction State with Events +-------------------------------------- + +See the section :ref:`session_transaction_events` for an overview +of the available event hooks for session transaction state changes. + .. _session_external_transaction: Joining a Session into an External Transaction (such as for test suites) @@ -513,3 +519,4 @@ everything is rolled back. session.begin_nested() # ... the tearDown() method stays the same + |
