]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- added "cascade delete" behavior to "dynamic" relations just like
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 7 Dec 2007 16:13:19 +0000 (16:13 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 7 Dec 2007 16:13:19 +0000 (16:13 +0000)
that of regular relations.  if passive_deletes flag (also just added)
is not set, a delete of the parent item will trigger a full load of
the child items so that they can be deleted or updated accordingly.

CHANGES
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/dynamic.py
test/orm/dynamic.py

diff --git a/CHANGES b/CHANGES
index 99a7edcff09831f96a513567bab4e68c94341d56..ae64f385cbe5e73e0b7c644785ca4d02db18e768 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -53,6 +53,11 @@ CHANGES
      have not, you might notice your apps using a lot fewer queries than
      before in some situations. [ticket:871]
 
+   - added "cascade delete" behavior to "dynamic" relations just like
+     that of regular relations.  if passive_deletes flag (also just added)
+     is not set, a delete of the parent item will trigger a full load of 
+     the child items so that they can be deleted or updated accordingly.
+     
    - query.get() and query.load() do not take existing filter or other
      criterion into account; these methods *always* look up the given id
      in the database or return the current instance from the identity map, 
index 7f5672371bd8f2299ba846ab70138311ec31498f..7ce298f71347860a01db884a63f15c344180afdd 100644 (file)
@@ -245,7 +245,8 @@ 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=None, remote_side=None, enable_typechecks=True,
+    passive_deletes=False):
     """construct a dynamically-loading mapper property.
     
     This property is similar to relation(), except read operations
@@ -263,6 +264,7 @@ def dynamic_loader(argument, secondary=None, primaryjoin=None, secondaryjoin=Non
     return PropertyLoader(argument, secondary=secondary, primaryjoin=primaryjoin, 
             secondaryjoin=secondaryjoin, entity_name=entity_name, foreign_keys=foreign_keys, backref=backref, 
             post_update=post_update, cascade=cascade, remote_side=remote_side, enable_typechecks=enable_typechecks, 
+            passive_deletes=passive_deletes,
             strategy_class=DynaLoader)
 
 #def _relation_loader(mapper, secondary=None, primaryjoin=None, secondaryjoin=None, lazy=True, **kwargs):
index 0c49bcfc39efbb3faf2d606831f25ee219d51c7d..f5662a933d4f399bcfac89cfcc5288e2c6658d47 100644 (file)
@@ -1,8 +1,8 @@
 """'dynamic' collection API.  returns Query() objects on the 'read' side, alters
 a special AttributeHistory on the 'write' side."""
 
-from sqlalchemy import exceptions
-from sqlalchemy.orm import attributes, object_session
+from sqlalchemy import exceptions, util
+from sqlalchemy.orm import attributes, object_session, util as mapperutil
 from sqlalchemy.orm.query import Query
 from sqlalchemy.orm.mapper import has_identity, object_mapper
 
@@ -23,7 +23,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
         state.dict[self.key] = CollectionHistory(self, state)
 
     def get_collection(self, state, user_data=None):
-        return self.get_history(state)._added_items
+        return self.get_history(state, passive=True)._added_items
         
     def set(self, state, value, initiator):
         if initiator is self:
@@ -39,19 +39,23 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
         
     def get_history(self, state, passive=False):
         try:
-            return state.dict[self.key]
+            c = state.dict[self.key]
         except KeyError:
             state.dict[self.key] = c = CollectionHistory(self, state)
+
+        if not passive:
+            return CollectionHistory(self, state, apply_to=c)
+        else:
             return c
 
     def append(self, state, value, initiator, passive=False):
         if initiator is not self:
-            self.get_history(state)._added_items.append(value)
+            self.get_history(state, passive=True)._added_items.append(value)
             self.fire_append_event(state, value, initiator)
     
     def remove(self, state, value, initiator, passive=False):
         if initiator is not self:
-            self.get_history(state)._deleted_items.append(value)
+            self.get_history(state, passive=True)._deleted_items.append(value)
             self.fire_remove_event(state, value, initiator)
 
             
@@ -64,7 +68,7 @@ class AppenderQuery(Query):
     def __session(self):
         instance = self.state.obj()
         sess = object_session(instance)
-        if sess is not None and instance in sess and sess.autoflush:
+        if sess is not None and self.autoflush and sess.autoflush and instance in sess:
             sess.flush()
         if not has_identity(instance):
             return None
@@ -74,14 +78,14 @@ class AppenderQuery(Query):
     def __iter__(self):
         sess = self.__session()
         if sess is None:
-            return iter(self.attr.get_history(self.state)._added_items)
+            return iter(self.attr.get_history(self.state, passive=True)._added_items)
         else:
             return iter(self._clone(sess))
 
     def __getitem__(self, index):
         sess = self.__session()
         if sess is None:
-            return self.attr.get_history(self.state)._added_items.__getitem__(index)
+            return self.attr.get_history(self.state, passive=True)._added_items.__getitem__(index)
         else:
             return self._clone(sess).__getitem__(index)
 
@@ -96,7 +100,7 @@ class AppenderQuery(Query):
                 try:
                     sess = object_mapper(instance).get_session()
                 except exceptions.InvalidRequestError:
-                    raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (self.instance.__class__, self.key))
+                    raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (mapperutil.instance_str(instance), self.attr.key))
 
         return sess.query(self.attr.target_mapper).with_parent(instance)
 
@@ -106,7 +110,7 @@ class AppenderQuery(Query):
             oldlist = list(self)
         else:
             oldlist = []
-        self.attr.get_history(self.state).replace(oldlist, collection)
+        self.attr.get_history(self.state, passive=True).replace(oldlist, collection)
         return oldlist
         
     def append(self, item):
@@ -119,12 +123,19 @@ class AppenderQuery(Query):
 class CollectionHistory(attributes.AttributeHistory): 
     """Overrides AttributeHistory to receive append/remove events directly."""
 
-    def __init__(self, attr, state):
-        self._deleted_items = []
-        self._added_items = []
-        self._unchanged_items = []
-        self._state = state
-        
+    def __init__(self, attr, state, apply_to=None):
+        if apply_to:
+            deleted = util.IdentitySet(apply_to._deleted_items)
+            added = apply_to._added_items
+            coll = AppenderQuery(attr, state).autoflush(False)
+            self._unchanged_items = [o for o in util.IdentitySet(coll) if o not in deleted]
+            self._added_items = apply_to._added_items
+            self._deleted_items = apply_to._deleted_items
+        else:
+            self._deleted_items = []
+            self._added_items = []
+            self._unchanged_items = []
+            
     def replace(self, olditems, newitems):
         self._added_items = newitems
         self._deleted_items = olditems
index fa8828c20a97b2dd5783fbcf50ba5a2bda2df836..096d945781ac2b583416fa32596d855ab1ffe287 100644 (file)
@@ -24,6 +24,21 @@ class DynamicTest(FixtureTest):
         assert [User(id=7, addresses=[Address(id=1, email_address='jack@bean.com')])] == q.filter(User.id==7).all()
         assert fixtures.user_address_result == q.all()
 
+    def test_backref(self):
+        mapper(Address, addresses, properties={
+            'user':relation(User, backref=backref('addresses', lazy='dynamic'))
+        })
+        mapper(User, users)
+        
+        sess = create_session()
+        ad = sess.query(Address).get(1)
+        def go():
+            ad.user = None
+        self.assert_sql_count(testbase.db, go, 1)
+        sess.flush()
+        u = sess.query(User).get(7)
+        assert ad not in u.addresses
+        
     def test_no_count(self):
         mapper(User, users, properties={
             'addresses':dynamic_loader(mapper(Address, addresses))
@@ -101,9 +116,15 @@ class FlushTest(FixtureTest):
         sess.delete(u.addresses[4])
         sess.delete(u.addresses[3])
         assert [Address(email_address='a'), Address(email_address='b'), Address(email_address='d')] == list(u.addresses)
-
+        
         sess.delete(u)
+        
+        # u.addresses relation will have to force the load
+        # of all addresses so that they can be updated
+        sess.flush()
         sess.close()
+        
+        assert testbase.db.scalar(addresses.count(addresses.c.user_id != None)) ==0
 
     @testing.fails_on('maxdb')
     def test_remove_orphans(self):