From f6d48da9de778ca519f9370b7e448226851ed493 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 19 Mar 2007 17:54:29 +0000 Subject: [PATCH] - many-to-many table will be properly handled even for operations that occur on the "backref" side of the operation [ticket:249] --- CHANGES | 3 +++ lib/sqlalchemy/orm/dependency.py | 21 ++++++++++++++++----- lib/sqlalchemy/orm/properties.py | 4 ++++ lib/sqlalchemy/orm/unitofwork.py | 5 +++-- test/orm/manytomany.py | 26 ++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 08febf731c..0f95dbab63 100644 --- 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 diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index c2d56fde82..b42995e0c5 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -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() diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index dc10a68552..45a0f9517e 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -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.""" diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 70f0e1d0b3..705dce1126 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -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 diff --git a/test/orm/manytomany.py b/test/orm/manytomany.py index e8076b9e07..f6e9197a2c 100644 --- a/test/orm/manytomany.py +++ b/test/orm/manytomany.py @@ -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): -- 2.47.2