]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- the "save-update" cascade will now cascade the pending *removed*
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 30 Jan 2010 18:28:37 +0000 (18:28 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 30 Jan 2010 18:28:37 +0000 (18:28 +0000)
values from a scalar or collection attribute into the new session
during an add() operation.  This so that the flush() operation
will also delete or modify rows of those disconnected items.

CHANGES
doc/build/session.rst
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/orm/session.py
test/orm/test_cascade.py

diff --git a/CHANGES b/CHANGES
index 1ee77d6f769ee2cd8b3ddc389d85f2f41accb184..fa9d2366d984ecf722982f52041e9e392df3a371 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -91,6 +91,11 @@ CHANGES
        example of how to integrate Beaker with SQLAlchemy.  See
        the notes in the "examples" note below.
   
+  - the "save-update" cascade will now cascade the pending *removed*
+    values from a scalar or collection attribute into the new session 
+    during an add() operation.  This so that the flush() operation
+    will also delete or modify rows of those disconnected items.
+    
   - Using a "dynamic" loader with a "secondary" table now produces
     a query where the "secondary" table is *not* aliased.  This
     allows the secondary Table object to be used in the "order_by"
index cbd33357b2fe3dd782f641758b13801cf837c264..9fa03a783398f2bf454d95511e06b9d6146974aa 100644 (file)
@@ -395,7 +395,7 @@ Cascading is configured by setting the ``cascade`` keyword argument on a ``relat
 
 The above mapper specifies two relations, ``items`` and ``customer``.  The ``items`` relationship specifies "all, delete-orphan" as its ``cascade`` value, indicating that all  ``add``, ``merge``, ``expunge``, ``refresh`` ``delete`` and ``expire`` operations performed on a parent ``Order`` instance should also be performed on the child ``Item`` instances attached to it.  The ``delete-orphan`` cascade value additionally indicates that if an ``Item`` instance is no longer associated with an ``Order``, it should also be deleted.  The "all, delete-orphan" cascade argument allows a so-called *lifecycle* relationship between an ``Order`` and an ``Item`` object.
 
-The ``customer`` relationship specifies only the "save-update" cascade value, indicating most operations will not be cascaded from a parent ``Order`` instance to a child ``User`` instance except for the ``add()`` operation.  "save-update" cascade indicates that an ``add()`` on the parent will cascade to all child items, and also that items added to a parent which is already present in the session will also be added.
+The ``customer`` relationship specifies only the "save-update" cascade value, indicating most operations will not be cascaded from a parent ``Order`` instance to a child ``User`` instance except for the ``add()`` operation.  "save-update" cascade indicates that an ``add()`` on the parent will cascade to all child items, and also that items added to a parent which is already present in the session will also be added.  "save-update" cascade also cascades the *pending history* of a relation()-based 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 new session - this so that foreign key clear operations and deletions will take place (new in 0.6).
 
 Note that the ``delete-orphan`` cascade only functions for relationships where the target object can have a single parent at a time, meaning it is only appropriate for one-to-one or one-to-many relationships.  For a :func:`~sqlalchemy.orm.relation` which establishes one-to-one via a local foreign key, i.e. a many-to-one that stores only a single parent, or one-to-one/one-to-many via a "secondary" (association) table, a warning will be issued if ``delete-orphan`` is configured.  To disable this warning, also specify the ``single_parent=True`` flag on the relationship, which constrains objects to allow attachment to only one parent at a time.
 
index 3021f99042fd13787b8c87fd1f46b39ad9ec36b1..574df7d91aec212fc9d477a102a0d8c6e6c98423 100644 (file)
@@ -1227,13 +1227,17 @@ class History(tuple):
         return self != HISTORY_BLANK
     
     def sum(self):
-        return self.added + self.unchanged + self.deleted
+        return (self.added or []) +\
+                (self.unchanged or []) +\
+                (self.deleted or [])
     
     def non_deleted(self):
-        return self.added + self.unchanged
+        return (self.added or []) +\
+                (self.unchanged or [])
     
     def non_added(self):
-        return self.unchanged + self.deleted
+        return (self.unchanged or []) +\
+                (self.deleted or [])
     
     def has_changes(self):
         return bool(self.added or self.deleted)
index 28f2f4dfd093341869340ed3d34d10ba9f362af5..486aa2e4b735e79cecf4785d4f4f64a56c81bf8b 100644 (file)
@@ -694,17 +694,24 @@ class RelationProperty(StrategizedProperty):
         else:
             passive = attributes.PASSIVE_OFF
 
-        mapper = self.mapper.primary_mapper()
-        instances = state.value_as_iterable(self.key, passive=passive)
+        if type_ == 'save-update':
+            instances = attributes.get_state_history(state, self.key, passive=passive).sum()
+        else:
+            instances = state.value_as_iterable(self.key, passive=passive)
+        
         if instances:
             for c in instances:
-                if c is not None and c not in visited_instances and (halt_on is None or not halt_on(c)):
+                if c is not None and c not in visited_instances and \
+                                        (halt_on is None or not halt_on(c)):
                     if not isinstance(c, self.mapper.class_):
                         raise AssertionError("Attribute '%s' on class '%s' doesn't handle objects "
-                                    "of type '%s'" % (self.key, str(self.parent.class_), str(c.__class__)))
+                                    "of type '%s'" % (self.key, 
+                                                        str(self.parent.class_), 
+                                                        str(c.__class__)))
                     visited_instances.add(c)
 
-                    # cascade using the mapper local to this object, so that its individual properties are located
+                    # cascade using the mapper local to this 
+                    # object, so that its individual properties are located
                     instance_mapper = object_mapper(c)
                     yield (c, instance_mapper, attributes.instance_state(c))
 
index 77e15be5a92d62bd5763dfd6cf9fd7f32a39e6e9..d15fbbc1a7cc69d3aca478107eee74a40c8cbf4a 100644 (file)
@@ -1049,7 +1049,8 @@ class Session(object):
         self._cascade_save_or_update(state)
 
     def _cascade_save_or_update(self, state):
-        for state, mapper in _cascade_unknown_state_iterator('save-update', state, halt_on=lambda c:c in self):
+        for state, mapper in _cascade_unknown_state_iterator(
+                                    'save-update', state, halt_on=lambda c:c in self):
             self._save_or_update_impl(state)
 
     def delete(self, instance):
index 3b9481dae9e4207c021ad65f008043f9d9c93594..7264ed82439b2f87c2c7db46272ef2e15c9dcfd0 100644 (file)
@@ -2,7 +2,7 @@
 from sqlalchemy.test.testing import assert_raises, assert_raises_message
 from sqlalchemy import Integer, String, ForeignKey, Sequence, exc as sa_exc
 from sqlalchemy.test.schema import Table, Column
-from sqlalchemy.orm import mapper, relation, create_session, class_mapper, backref
+from sqlalchemy.orm import mapper, relation, create_session, sessionmaker, class_mapper, backref
 from sqlalchemy.orm import attributes, exc as orm_exc
 from sqlalchemy.test import testing
 from sqlalchemy.test.testing import eq_
@@ -56,7 +56,26 @@ class O2MCascadeTest(_fixtures.FixtureTest):
         sess.add(o5)
         assert_raises_message(orm_exc.FlushError, "is an orphan", sess.flush)
 
-
+    @testing.resolve_artifact_names
+    def test_save_update_sends_pending(self):
+        """test that newly added and deleted collection items are cascaded on save-update"""
+        
+        sess = sessionmaker(expire_on_commit=False)()
+        o1, o2, o3 = Order(description='o1'), Order(description='o2'), Order(description='o3')
+        u = User(name='jack', orders=[o1, o2])
+        sess.add(u)
+        sess.commit()
+        sess.close()
+        
+        u.orders.append(o3)
+        u.orders.remove(o1)
+        
+        sess.add(u)
+        assert o1 in sess
+        assert o2 in sess
+        assert o3 in sess
+        sess.commit()
+        
     @testing.resolve_artifact_names
     def test_delete(self):
         sess = create_session()
@@ -401,6 +420,26 @@ class M2OCascadeTest(_base.MappedTest):
         assert prefs.count().scalar() == 2
         assert extra.count().scalar() == 2
 
+    @testing.resolve_artifact_names
+    def test_save_update_sends_pending(self):
+        """test that newly added and deleted scalar items are cascaded on save-update"""
+
+        sess = sessionmaker(expire_on_commit=False)()
+        p1, p2 = Pref(data='p1'), Pref(data='p2')
+        
+        
+        u = User(name='jack', pref=p1)
+        sess.add(u)
+        sess.commit()
+        sess.close()
+
+        u.pref = p2
+        
+        sess.add(u)
+        assert p1 in sess
+        assert p2 in sess
+        sess.commit()
+
     @testing.fails_on('maxdb', 'FIXME: unknown')
     @testing.resolve_artifact_names
     def test_orphan_on_update(self):