summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2008-07-19 18:18:50 +0000
committerMike Bayer <mike_mp@zzzcomputing.com>2008-07-19 18:18:50 +0000
commite78d06a4db273cd1b5b2b2212a5b2d5d15270d7c (patch)
treeee9c4222350e1a2bd515d7ef9c0b9fb79dae31f2
parent5c75aed9be1941144c390673709861e86c1a3e14 (diff)
downloadsqlalchemy-e78d06a4db273cd1b5b2b2212a5b2d5d15270d7c.tar.gz
- reverted r4955, that was wrong. The backref responsible for the operation is the one where the "cascade" option should take effect.
- can use None as a value for cascade. - documented cascade options in docstring, [ticket:1064]
-rw-r--r--CHANGES11
-rw-r--r--lib/sqlalchemy/orm/__init__.py20
-rw-r--r--lib/sqlalchemy/orm/properties.py4
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py25
-rw-r--r--lib/sqlalchemy/orm/util.py5
-rw-r--r--test/orm/cascade.py48
6 files changed, 54 insertions, 59 deletions
diff --git a/CHANGES b/CHANGES
index b3a95beeb..c518e9e0e 100644
--- a/CHANGES
+++ b/CHANGES
@@ -9,14 +9,9 @@ CHANGES
"0.4.7".
- orm
- - Cascade rules can now function unidirectionally on an
- otherwise bidirectional relation(), taking only
- the cascade behavior of the operation's initiator into
- account. This means that a cascade of "none" on one
- side won't trigger a "save-update" operation when an element
- is attached to that relation, even if the backref
- does indicate "save-update" cascade.
-
+ - The 'cascade' parameter to relation() accepts None
+ as a value, which is equivalent to no cascades.
+
- Added a new SessionExtension hook called after_attach().
This is called at the point of attachment for objects
via add(), add_all(), delete(), and merge().
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 45982ab2e..9c8ff35fd 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -173,8 +173,22 @@ def relation(argument, secondary=None, **kwargs):
configurability.
cascade
- a string list of cascade rules which determines how persistence
- operations should be "cascaded" from parent to child.
+ 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".
+ Available cascades are:
+ save-update - cascade the "add()" operation
+ (formerly known as save() and update())
+ merge - cascade the "merge()" operation
+ expunge - cascade the "expunge()" operation
+ delete - cascade the "delete()" operation
+ delete-orphan - if an item of the child's type with no parent is detected,
+ mark it for deletion. Note that this option prevents a pending item
+ of the child's class from being persisted without a parent
+ present.
+ refresh-expire - cascade the expire() and refresh() operations
+ all - shorthand for "save-update,merge, refresh-expire, expunge, delete"
collection_class
a class or function that returns a new list-holding object. will be
@@ -325,7 +339,7 @@ def relation(argument, secondary=None, **kwargs):
return PropertyLoader(argument, secondary=secondary, **kwargs)
def dynamic_loader(argument, secondary=None, primaryjoin=None, secondaryjoin=None, entity_name=None,
- foreign_keys=None, backref=None, post_update=False, cascade=None, remote_side=None, enable_typechecks=True,
+ foreign_keys=None, backref=None, post_update=False, cascade=False, remote_side=None, enable_typechecks=True,
passive_deletes=False, order_by=None):
"""Construct a dynamically-loading mapper property.
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 125de37f9..47d20878f 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -236,7 +236,7 @@ class PropertyLoader(StrategizedProperty):
backref=None,
_is_backref=False,
post_update=False,
- cascade=None,
+ cascade=False,
viewonly=False, lazy=True,
collection_class=None, passive_deletes=False,
passive_updates=True, remote_side=None,
@@ -280,7 +280,7 @@ class PropertyLoader(StrategizedProperty):
self._reverse_property = None
- if cascade is not None:
+ if cascade is not False:
self.cascade = CascadeOptions(cascade)
else:
self.cascade = CascadeOptions("save-update, merge")
diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py
index ac73f979d..e3a100661 100644
--- a/lib/sqlalchemy/orm/unitofwork.py
+++ b/lib/sqlalchemy/orm/unitofwork.py
@@ -39,25 +39,22 @@ class UOWEventHandler(interfaces.AttributeExtension):
def __init__(self, key):
self.key = key
- def _target_mapper(self, state):
- prop = _state_mapper(state).get_property(self.key)
- return prop.mapper
-
def append(self, state, item, initiator):
# process "save_update" cascade rules for when an instance is appended to the list of another instance
sess = _state_session(state)
if sess:
- initiating_property = initiator.class_manager[initiator.key].property
- if initiating_property.cascade.save_update and item not in sess:
- sess.save_or_update(item, entity_name=self._target_mapper(state).entity_name)
+ prop = _state_mapper(state).get_property(self.key)
+ if prop.cascade.save_update and item not in sess:
+ sess.save_or_update(item, entity_name=prop.mapper.entity_name)
def remove(self, state, item, initiator):
sess = _state_session(state)
if sess:
- initiating_property = initiator.class_manager[initiator.key].property
+ prop = _state_mapper(state).get_property(self.key)
# expunge pending orphans
- if initiating_property.cascade.delete_orphan and item in sess.new:
- if self._target_mapper(state)._is_orphan(attributes.instance_state(item)):
+ if prop.cascade.delete_orphan and \
+ item in sess.new and \
+ prop.mapper._is_orphan(attributes.instance_state(item)):
sess.expunge(item)
def set(self, state, newvalue, oldvalue, initiator):
@@ -66,10 +63,10 @@ class UOWEventHandler(interfaces.AttributeExtension):
return
sess = _state_session(state)
if sess:
- initiating_property = initiator.class_manager[initiator.key].property
- if newvalue is not None and initiating_property.cascade.save_update and newvalue not in sess:
- sess.save_or_update(newvalue, entity_name=self._target_mapper(state).entity_name)
- if initiating_property.cascade.delete_orphan and oldvalue in sess.new:
+ prop = _state_mapper(state).get_property(self.key)
+ if newvalue is not None and prop.cascade.save_update and newvalue not in sess:
+ sess.save_or_update(newvalue, entity_name=prop.mapper.entity_name)
+ if prop.cascade.delete_orphan and oldvalue in sess.new:
sess.expunge(oldvalue)
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 09990615a..7f11aaca9 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -22,7 +22,10 @@ class CascadeOptions(object):
"""Keeps track of the options sent to relation().cascade"""
def __init__(self, arg=""):
- values = set(c.strip() for c in arg.split(','))
+ if not arg:
+ values = set()
+ else:
+ values = set(c.strip() for c in arg.split(','))
self.delete_orphan = "delete-orphan" in values
self.delete = "delete" in values or "all" in values
self.save_update = "save-update" in values or "all" in values
diff --git a/test/orm/cascade.py b/test/orm/cascade.py
index 10f3cbf38..0f13644aa 100644
--- a/test/orm/cascade.py
+++ b/test/orm/cascade.py
@@ -1,7 +1,7 @@
import testenv; testenv.configure_for_tests()
from testlib.sa import Table, Column, Integer, String, ForeignKey, Sequence
-from testlib.sa.orm import mapper, relation, create_session, class_mapper
+from testlib.sa.orm import mapper, relation, create_session, class_mapper, backref
from testlib.sa.orm import attributes, exc as orm_exc
from testlib import testing
from testlib.testing import eq_
@@ -157,14 +157,14 @@ class O2MCascadeTest(_fixtures.FixtureTest):
class NoSaveCascadeTest(_fixtures.FixtureTest):
"""test that backrefs don't force save-update cascades to occur
- when they're not desired in the forwards direction."""
+ when the cascade initiated from the forwards side."""
@testing.resolve_artifact_names
def test_unidirectional_cascade_o2m(self):
mapper(Order, orders)
mapper(User, users, properties = dict(
orders = relation(
- Order, cascade="none", backref="user")
+ Order, backref=backref("user", cascade=None))
))
sess = create_session()
@@ -176,51 +176,37 @@ class NoSaveCascadeTest(_fixtures.FixtureTest):
assert o1 in sess
sess.clear()
- u1 = User()
- sess.add(u1)
+
o1 = Order()
- o1.user = u1
- assert u1 in sess
+ u1 = User(orders=[o1])
+ sess.add(o1)
+ assert u1 not in sess
assert o1 in sess
@testing.resolve_artifact_names
def test_unidirectional_cascade_m2o(self):
mapper(Order, orders, properties={
- 'user':relation(User, cascade="none", backref="orders")
+ 'user':relation(User, backref=backref("orders", cascade=None))
})
mapper(User, users)
sess = create_session()
-
- o1 = Order()
- sess.add(o1)
- o1.user = u1 = User()
- assert u1 not in sess
- assert o1 in sess
-
- sess.clear()
-
+
+ u1 = User()
+ sess.add(u1)
o1 = Order()
- sess.add(o1)
- u1 = User(orders=[o1])
+ o1.user = u1
+ assert o1 not in sess
assert u1 in sess
- assert o1 in sess
sess.clear()
-
- o1 = Order()
- o1.user = u1 = User()
- sess.add(o1)
- assert u1 not in sess
- assert o1 in sess
-
- sess.clear()
+ u1 = User()
o1 = Order()
- u1 = User(orders=[o1])
+ o1.user = u1
sess.add(u1)
+ assert o1 not in sess
assert u1 in sess
- assert o1 in sess
@testing.resolve_artifact_names
def test_unidirectional_cascade_m2m(self):
@@ -245,7 +231,7 @@ class NoSaveCascadeTest(_fixtures.FixtureTest):
sess.add(i1)
k1.items.append(i1)
assert i1 in sess
- assert k1 in sess
+ assert k1 not in sess
class O2MCascadeNoOrphanTest(_fixtures.FixtureTest):