]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- save-update and delete-orphan cascade event handler
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 18 Jul 2008 22:11:22 +0000 (22:11 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 18 Jul 2008 22:11:22 +0000 (22:11 +0000)
now considers the cascade rules of the event initiator only, not the local
attribute.  This way the cascade of the initiator controls the behavior
regardless of backref events.

CHANGES
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/unitofwork.py
test/orm/cascade.py

diff --git a/CHANGES b/CHANGES
index 13b9a979be2fba68bf1e5c0e1f7a16b0d4fefc76..b3a95beeb81c932242f458b720f072b5f894d8f7 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -9,6 +9,14 @@ 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.
+      
     - 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().
index 72d527e62c64f8f5bb6af2b6b5feca8c65603511..7eecc33202870c33163b8542dcf2601725f5fc68 100644 (file)
@@ -306,7 +306,6 @@ class AbstractRelationLoader(LoaderStrategy):
             uselist=self.uselist, 
             useobject=True, 
             extension=attribute_ext, 
-            cascade=self.parent_property.cascade,  
             trackparent=True, 
             typecallable=self.parent_property.collection_class, 
             callable_=callable_, 
index b4e649262a522b9e1ade6f864ac78135a230f1fe..ac73f979dd12ee4320e1029a50c85ae71b9f0aff 100644 (file)
@@ -36,10 +36,8 @@ class UOWEventHandler(interfaces.AttributeExtension):
     session cascade operations.
     """
 
-    def __init__(self, key, class_, cascade):
+    def __init__(self, key):
         self.key = key
-        self.class_ = class_
-        self.cascade = cascade
     
     def _target_mapper(self, state):
         prop = _state_mapper(state).get_property(self.key)
@@ -49,14 +47,16 @@ class UOWEventHandler(interfaces.AttributeExtension):
         # process "save_update" cascade rules for when an instance is appended to the list of another instance
         sess = _state_session(state)
         if sess:
-            if self.cascade.save_update and item not in 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)
 
     def remove(self, state, item, initiator):
         sess = _state_session(state)
         if sess:
+            initiating_property = initiator.class_manager[initiator.key].property
             # expunge pending orphans
-            if self.cascade.delete_orphan and item in sess.new:
+            if initiating_property.cascade.delete_orphan and item in sess.new:
                 if self._target_mapper(state)._is_orphan(attributes.instance_state(item)):
                     sess.expunge(item)
 
@@ -66,9 +66,10 @@ class UOWEventHandler(interfaces.AttributeExtension):
             return
         sess = _state_session(state)
         if sess:
-            if newvalue is not None and self.cascade.save_update and newvalue not in 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 self.cascade.delete_orphan and oldvalue in sess.new:
+            if initiating_property.cascade.delete_orphan and oldvalue in sess.new:
                 sess.expunge(oldvalue)
 
 
@@ -77,13 +78,12 @@ def register_attribute(class_, key, *args, **kwargs):
     to new InstrumentedAttributes.
     """
     
-    cascade = kwargs.pop('cascade', None)
     useobject = kwargs.get('useobject', False)
     if useobject:
         # for object-holding attributes, instrument UOWEventHandler
         # to process per-attribute cascades
         extension = util.to_list(kwargs.pop('extension', None) or [])
-        extension.insert(0, UOWEventHandler(key, class_, cascade=cascade))
+        extension.insert(0, UOWEventHandler(key))
         kwargs['extension'] = extension
     return attributes.register_attribute(class_, key, *args, **kwargs)
     
index 4df4559468757c5e3f57a0f159cc01be38f19b76..10f3cbf38758afcffb537c1a33d1d0d39d6f6394 100644 (file)
@@ -155,6 +155,99 @@ class O2MCascadeTest(_fixtures.FixtureTest):
         assert users.count().scalar() == 1
         assert orders.count().scalar() == 0
 
+class NoSaveCascadeTest(_fixtures.FixtureTest):
+    """test that backrefs don't force save-update cascades to occur
+    when they're not desired in the forwards direction."""
+    
+    @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")
+        ))
+        
+        sess = create_session()
+        
+        o1 = Order()
+        sess.add(o1)
+        u1 = User(orders=[o1])
+        assert u1 not in sess
+        assert o1 in sess
+        
+        sess.clear()
+        u1 = User()
+        sess.add(u1)
+        o1 = Order()
+        o1.user = u1
+        assert u1 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")
+        })
+        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()
+
+        o1 = Order()
+        sess.add(o1)
+        u1 = User(orders=[o1])
+        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()
+
+        o1 = Order()
+        u1 = User(orders=[o1])
+        sess.add(u1)
+        assert u1 in sess
+        assert o1 in sess
+
+    @testing.resolve_artifact_names
+    def test_unidirectional_cascade_m2m(self):
+        mapper(Item, items, properties={
+            'keywords':relation(Keyword, secondary=item_keywords, cascade="none", backref="items")
+        })
+        mapper(Keyword, keywords)
+
+        sess = create_session()
+
+        i1 = Item()
+        k1 = Keyword()
+        sess.add(i1)
+        i1.keywords.append(k1)
+        assert i1 in sess
+        assert k1 not in sess
+        
+        sess.clear()
+        
+        i1 = Item()
+        k1 = Keyword()
+        sess.add(i1)
+        k1.items.append(i1)
+        assert i1 in sess
+        assert k1 in sess
+        
+    
 class O2MCascadeNoOrphanTest(_fixtures.FixtureTest):
     run_inserts = None