- fairly epic rework of the cascade documentation

This commit is contained in:
Mike Bayer
2014-03-15 15:44:29 -04:00
parent 6d0f734a3d
commit 56e38b8e4e
5 changed files with 322 additions and 157 deletions
+2
View File
@@ -129,6 +129,8 @@ For example::
)
)
.. _on_update_on_delete:
ON UPDATE and ON DELETE
~~~~~~~~~~~~~~~~~~~~~~~
+12
View File
@@ -75,6 +75,18 @@ Glossary
>>> inspect(m1).attrs.data.history.added
"some data"
DDL
An acronym for *Data Definition Language*. DDL is the subset
of SQL that relational databases use to configure tables, constraints,
and other permanent objects within a database schema. SQLAlchemy
provides a rich API for constructing and emitting DDL expressions.
.. seealso::
:ref:`metadata_toplevel`
`DDL (via Wikipedia) <http://en.wikipedia.org/wiki/Data_definition_language>`_
discriminator
A result-set column which is used during :term:`polymorphic` loading
to determine what kind of mapped class should be applied to a particular
+2
View File
@@ -187,6 +187,8 @@ is accepted as well, matching the name of the table as stored in ``Base.metadata
secondary="association",
backref="parents")
.. _relationships_many_to_many_deletion:
Deleting Rows from the Many to Many Table
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+292 -97
View File
@@ -725,7 +725,7 @@ and made our ``a1`` object pending, as though we had added it. Now we have
Above, our ``a1`` is already pending in the session. The
subsequent :meth:`~.Session.merge` operation essentially
does nothing. Cascade can be configured via the ``cascade``
does nothing. Cascade can be configured via the :paramref:`~.relationship.cascade`
option on :func:`.relationship`, although in this case it
would mean removing the ``save-update`` cascade from the
``User.addresses`` relationship - and usually, that behavior
@@ -1287,28 +1287,29 @@ setting.
Cascades
========
Mappers support the concept of configurable **cascade** behavior on
Mappers support the concept of configurable :term:`cascade` behavior on
:func:`~sqlalchemy.orm.relationship` constructs. This refers
to how operations performed on a parent object relative to a
to how operations performed on a "parent" object relative to a
particular :class:`.Session` should be propagated to items
referred to by that relationship.
The default cascade behavior is usually suitable for
most situations, and the option is normally invoked explicitly
in order to enable ``delete`` and ``delete-orphan`` cascades,
which refer to how the relationship should be treated when
the parent is marked for deletion as well as when a child
is de-associated from its parent.
referred to by that relationship (e.g. "child" objects), and is
affected by the :paramref:`.relationship.cascade` option.
Cascade behavior is configured by setting the ``cascade`` keyword
argument on
The default behavior of cascade is limited to cascades of the
so-called :ref:`cascade_save_update` and :ref:`cascade_merge` settings.
The typical "alternative" setting for cascade is to add
the :ref:`cascade_delete` and :ref:`cascade_delete_orphan` options;
these settings are appropriate for related objects which only exist as
long as they are attached to their parent, and are otherwise deleted.
Cascade behavior is configured using the by changing the
:paramref:`~.relationship.cascade` option on
:func:`~sqlalchemy.orm.relationship`::
class Order(Base):
__tablename__ = 'order'
items = relationship("Item", cascade="all, delete-orphan")
customer = relationship("User", secondary=user_orders_table,
cascade="save-update")
customer = relationship("User", cascade="save-update")
To set cascades on a backref, the same flag can be used with the
:func:`~.sqlalchemy.orm.backref` function, which ultimately feeds
@@ -1321,109 +1322,303 @@ its arguments back into :func:`~sqlalchemy.orm.relationship`::
backref=backref("items", cascade="all, delete-orphan")
)
The default value of ``cascade`` is ``save-update, merge``.
The ``all`` symbol in the cascade options indicates that all
cascade flags should be enabled, with the exception of ``delete-orphan``.
Typically, cascade is usually left at its default, or configured
as ``all, delete-orphan``, indicating the child objects should be
treated as "owned" by the parent.
.. sidebar:: The Origins of Cascade
The list of available values which can be specified in ``cascade``
are as follows:
SQLAlchemy's notion of cascading behavior on relationships,
as well as the options to configure them, are primarily derived
from the similar feature in the Hibernate ORM; Hibernate refers
to "cascade" in a few places such as in
`Example: Parent/Child <https://docs.jboss.org/hibernate/orm/3.3/reference/en-US/html/example-parentchild.html>`_.
If cascades are confusing, we'll refer to their conclusion,
stating "The sections we have just covered can be a bit confusing.
However, in practice, it all works out nicely.".
* ``save-update`` - Indicates that when an object is placed into a
:class:`.Session`
via :meth:`.Session.add`, all the objects associated with it via this
:func:`~sqlalchemy.orm.relationship` should also be added to that
same :class:`.Session`. Additionally, if this object is already present in
a :class:`.Session`, child objects will be added to that session as they
are associated with this parent, i.e. as they are appended to lists,
added to sets, or otherwise associated with the parent.
The default value of :paramref:`~.relationship.cascade` is ``save-update, merge``.
The typical alternative setting for this parameter is either
``all`` or more commonly ``all, delete-orphan``. The ``all`` symbol
is a synonym for ``save-update, merge, refresh-expire, expunge, delete``,
and using it in conjunction with ``delete-orphan`` indicates that the child
object should follow along with its parent in all cases, and be deleted once
it is no longer associated with that parent.
``save-update`` cascade also cascades the *pending history* of the
target attribute, meaning that objects which were
removed from a scalar or collection attribute whose changes have not
yet been flushed are also placed into the target session. This
is because they may have foreign key attributes present which
will need to be updated to no longer refer to the parent.
The list of available values which can be specified for
the :paramref:`~.relationship.cascade` parameter are described in the following subsections.
The ``save-update`` cascade is on by default, and it's common to not
even be aware of it. It's customary that only a single call to
:meth:`.Session.add` against the lead object of a structure
has the effect of placing the full structure of
objects into the :class:`.Session` at once.
.. _cascade_save_update:
However, it can be turned off, which would
imply that objects associated with a parent would need to be
placed individually using :meth:`.Session.add` calls for
each one.
save-update
-----------
Another default behavior of ``save-update`` cascade is that it will
take effect in the reverse direction, that is, associating a child
with a parent when a backref is present means both relationships
are affected; the parent will be added to the child's session.
To disable this somewhat indirect session addition, use the
``cascade_backrefs=False`` option described below in
:ref:`backref_cascade`.
``save-update`` cacade indicates that when an object is placed into a
:class:`.Session` via :meth:`.Session.add`, all the objects associated
with it via this :func:`.relationship` should also be added to that
same :class:`.Session`. Suppose we have an object ``user1`` with two
related objects ``address1``, ``address2``::
* ``delete`` - This cascade indicates that when the parent object
is marked for deletion, the related objects should also be marked
for deletion. Without this cascade present, SQLAlchemy will
set the foreign key on a one-to-many relationship to NULL
when the parent object is deleted. When enabled, the row is instead
deleted.
>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]
``delete`` cascade is often used in conjunction with ``delete-orphan``
cascade, as is appropriate for an object whose foreign key is
not intended to be nullable. On some backends, it's also
a good idea to set ``ON DELETE`` on the foreign key itself;
see the section :ref:`passive_deletes` for more details.
If we add ``user1`` to a :class:`.Session`, it will also add
``address1``, ``address2`` implicitly::
Note that for many-to-many relationships which make usage of the
``secondary`` argument to :func:`~.sqlalchemy.orm.relationship`,
SQLAlchemy always emits
a DELETE for the association row in between "parent" and "child",
when the parent is deleted or whenever the linkage between a particular
parent and child is broken.
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True
* ``delete-orphan`` - This cascade adds behavior to the ``delete`` cascade,
such that a child object will be marked for deletion when it is
de-associated from the parent, not just when the parent is marked
for deletion. This is a common feature when dealing with a related
object that is "owned" by its parent, with a NOT NULL foreign key,
so that removal of the item from the parent collection results
in its deletion.
``save-update`` cascade also affects attribute operations for objects
that are already present in a :class:`.Session`. If we add a third
object, ``address3`` to the ``user1.addresses`` collection, it
becomes part of the state of that :class:`.Session`::
``delete-orphan`` cascade implies that each child object can only
have one parent at a time, so is configured in the vast majority of cases
on a one-to-many relationship. Setting it on a many-to-one or
many-to-many relationship is more awkward; for this use case,
SQLAlchemy requires that the :func:`~sqlalchemy.orm.relationship`
be configured with the :paramref:`~.relationship.single_parent` argument,
establishes Python-side validation that ensures the object
is associated with only one parent at a time.
>>> address3 = Address()
>>> user1.append(address3)
>>> address3 in sess
>>> True
* ``merge`` - This cascade indicates that the :meth:`.Session.merge`
operation should be propagated from a parent that's the subject
of the :meth:`.Session.merge` call down to referred objects.
This cascade is also on by default.
``save-update`` has the possibly surprising behavior which is that
persistent objects which were *removed* from a collection
or in some cases a scalar attribute
may also be pulled into the :class:`.Session` of a parent object; this is
so that the flush process may handle that related object appropriately.
This case can usually only arise if an object is removed from one :class:`.Session`
and added to another::
* ``refresh-expire`` - A less common option, indicates that the
:meth:`.Session.expire` operation should be propagated from a parent
down to referred objects. When using :meth:`.Session.refresh`,
the referred objects are expired only, but not actually refreshed.
>>> user1 = sess1.query(User).filter_by(id=1).first()
>>> address1 = user1.addresses[0]
>>> sess1.close() # user1, address1 no longer associated with sess1
>>> user1.addresses.remove(address1) # address1 no longer associated with user1
>>> sess2 = Session()
>>> sess2.add(user1) # ... but it still gets added to the new session,
>>> address1 in sess2 # because it's still "pending" for flush
True
* ``expunge`` - Indicate that when the parent object is removed
from the :class:`.Session` using :meth:`.Session.expunge`, the
operation should be propagated down to referred objects.
The ``save-update`` cascade is on by default, and is typically taken
for granted; it simplifies code by allowing a single call to
:meth:`.Session.add` to register an entire structure of objects within
that :class:`.Session` at once. While it can be disabled, there
is usually not a need to do so.
One case where ``save-update`` cascade does sometimes get in the way is in that
it takes place in both directions for bi-directional relationships, e.g.
backrefs, meaning that the association of a child object with a particular parent
can have the effect of the parent object being implicitly associated with that
child object's :class:`.Session`; this pattern, as well as how to modify its
behavior using the :paramref:`~.relationship.cascade_backrefs` flag,
is discussed in the section :ref:`backref_cascade`.
.. _cascade_delete:
delete
------
The ``delete`` cascade indicates that when a "parent" object
is marked for deletion, its related "child" objects should also be marked
for deletion. If for example we we have a relationship ``User.addresses``
with ``delete`` cascade configured::
class User(Base):
# ...
addresses = relationship("Address", cascade="save-update, merge, delete")
If using the above mapping, we have a ``User`` object and two
related ``Address`` objects::
>>> user1 = sess.query(User).filter_by(id=1).first()
>>> address1, address2 = user1.addresses
If we mark ``user1`` for deletion, after the flush operation proceeds,
``address1`` and ``address2`` will also be deleted:
.. sourcecode:: python+sql
>>> sess.delete(user1)
>>> sess.commit()
{opensql}DELETE FROM address WHERE address.id = ?
((1,), (2,))
DELETE FROM user WHERE user.id = ?
(1,)
COMMIT
Alternatively, if our ``User.addresses`` relationship does *not* have
``delete`` cascade, SQLAlchemy's default behavior is to instead de-associate
``address1`` and ``address2`` from ``user1`` by setting their foreign key
reference to ``NULL``. Using a mapping as follows::
class User(Base):
# ...
addresses = relationship("Address")
Upon deletion of a parent ``User`` object, the rows in ``address`` are not
deleted, but are instead de-associated:
.. sourcecode:: python+sql
>>> sess.delete(user1)
>>> sess.commit()
{opensql}UPDATE address SET user_id=? WHERE address.id = ?
(None, 1)
UPDATE address SET user_id=? WHERE address.id = ?
(None, 2)
DELETE FROM user WHERE user.id = ?
(1,)
COMMIT
``delete`` cascade is more often than not used in conjunction with
:ref:`cascade_delete_orphan` cascade, which will emit a DELETE for the related
row if the "child" object is deassociated from the parent. The combination
of ``delete`` and ``delete-orphan`` cascade covers both situations where
SQLAlchemy has to decide between setting a foreign key column to NULL versus
deleting the row entirely.
.. topic:: ORM-level "delete" cascade vs. FOREIGN KEY level "ON DELETE" cascade
The behavior of SQLAlchemy's "delete" cascade has a lot of overlap with the
``ON DELETE CASCADE`` feature of a database foreign key, as well
as with that of the ``ON DELETE SET NULL`` foreign key setting when "delete"
cascade is not specified. Database level "ON DELETE" cascades are specific to the
"FOREIGN KEY" construct of the relational database; SQLAlchemy allows
configuration of these schema-level constructs at the :term:`DDL` level
using options on :class:`.ForeignKeyConstraint` which are described
at :ref:`on_update_on_delete`.
It is important to note the differences between the ORM and the relational
database's notion of "cascade" as well as how they integrate:
* A database level ``ON DELETE`` cascade is configured effectively
on the **many-to-one** side of the relationship; that is, we configure
it relative to the ``FOREIGN KEY`` constraint that is the "many" side
of a relationship. At the ORM level, **this direction is reversed**.
SQLAlchemy handles the deletion of "child" objects relative to a
"parent" from the "parent" side, which means that ``delete`` and
``delete-orphan`` cascade are configured on the **one-to-many**
side.
* Database level foreign keys with no ``ON DELETE`` setting
are often used to **prevent** a parent
row from being removed, as it would necessarily leave an unhandled
related row present. If this behavior is desired in a one-to-many
relationship, SQLAlchemy's default behavior of setting a foreign key
to ``NULL`` can be caught in one of two ways:
* The easiest and most common is just to to set the
foreign-key-holding column to ``NOT NULL`` at the database schema
level. An attempt by SQLAlchemy to set the column to NULL will
fail with a simple NOT NULL constraint exception.
* The other, more special case way is to set the :paramref:`~.relationship.passive_deletes`
flag to the string ``"all"``. This has the effect of entirely
disabling SQLAlchemy's behavior of setting the foreign key column
to NULL, and a DELETE will be emitted for the parent row without
any affect on the child row, even if the child row is present
in memory. This may be desirable in the case when
database-level foreign key triggers, either special ``ON DELETE`` settings
or otherwise, need to be activated in all cases when a parent row is deleted.
* Database level ``ON DELETE`` cascade is **vastly more efficient**
than that of SQLAlchemy. The database can chain a series of cascade
operations across many relationships at once; e.g. if row A is deleted,
all the related rows in table B can be deleted, and all the C rows related
to each of those B rows, and on and on, all within the scope of a single
DELETE statement. SQLAlchemy on the other hand, in order to support
the cascading delete operation fully, has to individually load each
related collection in order to target all rows that then may have further
related collections. That is, SQLAlchemy isn't sophisticated enough
to emit a DELETE for all those related rows at once within this context.
* SQLAlchemy doesn't **need** to be this sophisticated, as we instead provide
smooth integration with the database's own ``ON DELETE`` functionality,
by using the :paramref:`~.relationship.passive_deletes` option in conjunction
with properly configured foreign key constraints. Under this behavior,
SQLAlchemy only emits DELETE for those rows that are already locally
present in the :class:`.Session`; for any collections that are unloaded,
it leaves them to the database to handle, rather than emitting a SELECT
for them. The section :ref:`passive_deletes` provides an example of this use.
* While database-level ``ON DELETE`` functionality works only on the "many"
side of a relationship, SQLAlchemy's "delete" cascade
has **limited** ability to operate in the *reverse* direction as well,
meaning it can be configured on the "many" side to delete an object
on the "one" side when the reference on the "many" side is deleted. However
this can easily result in constraint violations if there are other objects
referring to this "one" side from the "many", so it typically is only
useful when a relationship is in fact a "one to one". The
:paramref:`~.relationship.single_parent` flag should be used to establish
an in-Python assertion for this case.
When using a :func:`.relationship` that also includes a many-to-many
table using the :paramref:`~.relationship.secondary` option, SQLAlchemy's
delete cascade handles the rows in this many-to-many table automatically.
Just like, as described in :ref:`relationships_many_to_many_deletion`,
the addition or removal of an object from a many-to-many collection
results in the INSERT or DELETE of a row in the many-to-many table,
the ``delete`` cascade, when activated as the result of a parent object
delete operation, will DELETE not just the row in the "child" table but also
in the many-to-many table.
.. _cascade_delete_orphan:
delete-orphan
-------------
``delete-orphan`` cascade adds behavior to the ``delete`` cascade,
such that a child object will be marked for deletion when it is
de-associated from the parent, not just when the parent is marked
for deletion. This is a common feature when dealing with a related
object that is "owned" by its parent, with a NOT NULL foreign key,
so that removal of the item from the parent collection results
in its deletion.
``delete-orphan`` cascade implies that each child object can only
have one parent at a time, so is configured in the vast majority of cases
on a one-to-many relationship. Setting it on a many-to-one or
many-to-many relationship is more awkward; for this use case,
SQLAlchemy requires that the :func:`~sqlalchemy.orm.relationship`
be configured with the :paramref:`~.relationship.single_parent` argument,
establishes Python-side validation that ensures the object
is associated with only one parent at a time.
.. _cascade_merge:
merge
-----
``merge`` cascade indicates that the :meth:`.Session.merge`
operation should be propagated from a parent that's the subject
of the :meth:`.Session.merge` call down to referred objects.
This cascade is also on by default.
.. _cascade_refresh_expire:
refresh-expire
--------------
``refresh-expire`` is an uncommon option, indicating that the
:meth:`.Session.expire` operation should be propagated from a parent
down to referred objects. When using :meth:`.Session.refresh`,
the referred objects are expired only, but not actually refreshed.
.. _cascade_expunge:
expunge
-------
``expunge`` cascade indicates that when the parent object is removed
from the :class:`.Session` using :meth:`.Session.expunge`, the
operation should be propagated down to referred objects.
.. _backref_cascade:
Controlling Cascade on Backrefs
-------------------------------
The ``save-update`` cascade takes place on backrefs by default. This means
that, given a mapping such as this::
The :ref:`cascade_save_update` cascade by default takes place on attribute change events
emitted from backrefs. This is probably a confusing statement more
easily described through demonstration; it means that, given a mapping such as this::
mapper(Order, order_table, properties={
'items' : relationship(Item, backref='order')
+14 -60
View File
@@ -267,47 +267,20 @@ class RelationshipProperty(StrategizedProperty):
a comma-separated list of cascade rules which determines how
Session operations should be "cascaded" from parent to child.
This defaults to ``False``, which means the default cascade
should be used. The default value is ``"save-update, merge"``.
should be used - this default cascade is ``"save-update, merge"``.
Available cascades are:
* ``save-update`` - cascade the :meth:`.Session.add`
operation. This cascade applies both to future and
past calls to :meth:`.Session.add`,
meaning new items added to a collection or scalar relationship
get placed into the same session as that of the parent, and
also applies to items which have been removed from this
relationship but are still part of unflushed history.
* ``merge`` - cascade the :meth:`.Session.merge`
operation
* ``expunge`` - cascade the :meth:`.Session.expunge`
operation
* ``delete`` - cascade the :meth:`.Session.delete`
operation
* ``delete-orphan`` - if an item of the child's type is
detached from its parent, mark it for deletion.
.. versionchanged:: 0.7
This option does not prevent
a new instance of the child object from being persisted
without a parent to start with; to constrain against
that case, ensure the child's foreign key column(s)
is configured as NOT NULL
* ``refresh-expire`` - cascade the :meth:`.Session.expire`
and :meth:`.Session.refresh` operations
* ``all`` - shorthand for "save-update,merge, refresh-expire,
expunge, delete"
The available cascades are ``save-update``, ``merge``,
``expunge``, ``delete``, ``delete-orphan``, and ``refresh-expire``.
An additional option, ``all`` indicates shorthand for
``"save-update, merge, refresh-expire,
expunge, delete"``, and is often used as in ``"all, delete-orphan"``
to indicate that related objects should follow along with the
parent object in all cases, and be deleted when de-associated.
.. seealso::
:ref:`unitofwork_cascades` - Introductory documentation and
examples.
:ref:`unitofwork_cascades` - Full detail on each of the available
cascade options.
:ref:`tutorial_delete_cascade` - Tutorial example describing
a delete cascade.
@@ -315,33 +288,14 @@ class RelationshipProperty(StrategizedProperty):
:param cascade_backrefs=True:
a boolean value indicating if the ``save-update`` cascade should
operate along an assignment event intercepted by a backref.
When set to ``False``,
the attribute managed by this relationship will not cascade
an incoming transient object into the session of a
When set to ``False``, the attribute managed by this relationship
will not cascade an incoming transient object into the session of a
persistent parent, if the event is received via backref.
That is::
mapper(A, a_table, properties={
'bs':relationship(B, backref="a", cascade_backrefs=False)
})
If an ``A()`` is present in the session, assigning it to
the "a" attribute on a transient ``B()`` will not place
the ``B()`` into the session. To set the flag in the other
direction, i.e. so that ``A().bs.append(B())`` won't add
a transient ``A()`` into the session for a persistent ``B()``::
mapper(A, a_table, properties={
'bs':relationship(B,
backref=backref("a", cascade_backrefs=False)
)
})
.. seealso::
:ref:`backref_cascade` - Introductory documentation and
examples.
:ref:`backref_cascade` - Full discussion and examples on how
the :paramref:`~.relationship.cascade_backrefs` option is used.
:param collection_class:
a class or callable that returns a new list-holding object. will