]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- many-to-many table will be properly handled even for operations that
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 19 Mar 2007 17:54:29 +0000 (17:54 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 19 Mar 2007 17:54:29 +0000 (17:54 +0000)
occur on the "backref" side of the operation [ticket:249]

CHANGES
lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/orm/unitofwork.py
test/orm/manytomany.py

diff --git a/CHANGES b/CHANGES
index 08febf731c9b189b0146eda2ab79d34d7ee7dc2f..0f95dbab63c445403746924c956d765e53274381 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -91,6 +91,9 @@
       So that instances match up properly, the "uniquing" is disabled when 
       this feature is used.
 
+    - many-to-many table will be properly handled even for operations that
+      occur on the "backref" side of the operation [ticket:249]
+      
     - Query has add_entity() and add_column() generative methods.  these
       will add the given mapper/class or ColumnElement to the query at compile
       time, and apply them to the instances() method.  the user is responsible
index c2d56fde821d788c01ff0cde5c97759c7b33dd24..b42995e0c53652ea4818915c80cd52fc081f282d 100644 (file)
@@ -328,39 +328,50 @@ class ManyToManyDP(DependencyProcessor):
         # related mappers.  its dependency processor then populates the
         # association table.
 
-        if self.is_backref:
-            # if we are the "backref" half of a two-way backref
-            # relationship, let the other mapper handle inserting the rows
-            return
         stub = MapperStub(self.parent, self.mapper, self.key)
         uowcommit.register_dependency(self.parent, stub)
         uowcommit.register_dependency(self.mapper, stub)
         uowcommit.register_processor(stub, self, self.parent)
 
     def process_dependencies(self, task, deplist, uowcommit, delete = False):
-        #print self.mapper.table.name + " " + self.key + " " + repr(len(deplist)) + " process_dep isdelete " + repr(delete) + " direction " + repr(self.direction)
+        #print self.mapper.mapped_table.name + " " + self.key + " " + repr(len(deplist)) + " process_dep isdelete " + repr(delete) + " direction " + repr(self.direction)
         connection = uowcommit.transaction.connection(self.mapper)
         secondary_delete = []
         secondary_insert = []
+
+        if hasattr(self.prop, 'reverse_property'):
+            reverse_dep = getattr(self.prop.reverse_property, '_dependency_processor', None)
+        else:
+            reverse_dep = None
+            
         if delete:
             for obj in deplist:
                 childlist = self.get_object_dependencies(obj, uowcommit, passive=self.passive_deletes)
                 if childlist is not None:
                     for child in childlist.deleted_items() + childlist.unchanged_items():
+                        if reverse_dep and (reverse_dep, "manytomany", child, obj) in uowcommit.attributes:
+                            continue
                         associationrow = {}
                         self._synchronize(obj, child, associationrow, False, uowcommit)
                         secondary_delete.append(associationrow)
+                        uowcommit.attributes[(self, "manytomany", obj, child)] = True
         else:
             for obj in deplist:
                 childlist = self.get_object_dependencies(obj, uowcommit)
                 if childlist is None: continue
                 for child in childlist.added_items():
+                    if reverse_dep and (reverse_dep, "manytomany", child, obj) in uowcommit.attributes:
+                        continue
                     associationrow = {}
                     self._synchronize(obj, child, associationrow, False, uowcommit)
+                    uowcommit.attributes[(self, "manytomany", obj, child)] = True
                     secondary_insert.append(associationrow)
                 for child in childlist.deleted_items():
+                    if reverse_dep and (reverse_dep, "manytomany", child, obj) in uowcommit.attributes:
+                        continue
                     associationrow = {}
                     self._synchronize(obj, child, associationrow, False, uowcommit)
+                    uowcommit.attributes[(self, "manytomany", obj, child)] = True
                     secondary_delete.append(associationrow)
         if len(secondary_delete):
             secondary_delete.sort()
index dc10a68552c4f8dfcf3d224f65856bcc5ffb3a03..45a0f9517e84ffe960fb1bcb4b8afe834ebbab6e 100644 (file)
@@ -489,6 +489,10 @@ class BackRef(object):
                 prop.is_backref=True
                 if not prop.viewonly:
                     prop._dependency_processor.is_backref=True
+                    # reverse_property used by dependencies.ManyToManyDP to check
+                    # association table operations
+                    prop.reverse_property = mapper.props[self.key]
+                    mapper.props[self.key].reverse_property = prop
 
     def get_extension(self):
         """Return an attribute extension to use with this backreference."""
index 70f0e1d0b367eb9c17889f3e341037dd110716a6..705dce1126496f0bd0d4604690a485578352fc73 100644 (file)
@@ -232,7 +232,8 @@ class UOWTransaction(object):
         self.tasks = {}
         self.logger = logging.instance_logger(self)
         self.echo = uow.echo
-
+        self.attributes = {}
+        
     echo = logging.echo_property()
 
     def register_object(self, obj, isdelete = False, listonly = False, postupdate=False, post_update_cols=None, **kwargs):
@@ -337,7 +338,7 @@ class UOWTransaction(object):
         targettask = self.get_task_by_mapper(mapperfrom)
         up = UOWDependencyProcessor(processor, targettask)
         task.dependencies.add(up)
-
+        
     def execute(self):
         # ensure that we have a UOWTask for every mapper that will be involved
         # in the topological sort
index e8076b9e07749cccbadd6c3a4c21cdd289c05520..f6e9197a2c0532e381d1e200cf3343072123e558 100644 (file)
@@ -216,6 +216,32 @@ class M2MTest2(testbase.ORMTest):
         self.assert_(len(s.courses) == 3)
         del s.courses[1]
         self.assert_(len(s.courses) == 2)
+    
+    def test_delete(self):
+        """test that many-to-many table gets cleared out with deletion from the backref side"""
+        class Student(object):
+            def __init__(self, name=''):
+                self.name = name
+        class Course(object):
+            def __init__(self, name=''):
+                self.name = name
+        Student.mapper = mapper(Student, studentTbl)
+        Course.mapper = mapper(Course, courseTbl, properties = {
+            'students': relation(Student.mapper, enrolTbl, lazy=True, backref='courses')
+        })
+        sess = create_session()
+        s1 = Student('Student1')
+        c1 = Course('Course1')
+        c2 = Course('Course2')
+        c3 = Course('Course3')
+        s1.courses.append(c1)
+        s1.courses.append(c2)
+        c3.students.append(s1)
+        sess.save(s1)
+        sess.flush()
+        sess.delete(s1)
+        sess.flush()
+        assert enrolTbl.count().scalar() == 0
         
 class M2MTest3(testbase.ORMTest):
     def define_tables(self, metadata):