]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
latest overhaul to association objects, plus an actual unit test
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 27 May 2006 19:44:05 +0000 (19:44 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 27 May 2006 19:44:05 +0000 (19:44 +0000)
this change probably fixes [ticket:134]

lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/unitofwork.py
lib/sqlalchemy/orm/uowdumper.py
test/alltests.py
test/association.py [new file with mode: 0644]
test/relationships.py

index 8cc302bfd1a09378fc01ee8b2c97762b3e6dfa76..b122b621d4d25199c8affca476cec5996d9581ac 100644 (file)
@@ -9,7 +9,7 @@
 together to allow processing of scalar- and list-based dependencies at flush time."""
 
 from sync import ONETOMANY,MANYTOONE,MANYTOMANY
-from sqlalchemy import sql
+from sqlalchemy import sql, util
 
 def create_dependency_processor(key, syncrules, cascade, secondary=None, association=None, is_backref=False, post_update=False):
     types = {
@@ -309,41 +309,53 @@ class AssociationDP(OneToManyDP):
         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)
-        # manage association objects.
         for obj in deplist:
             childlist = self.get_object_dependencies(obj, uowcommit, passive=True)
             if childlist is None: continue
 
-            #print "DIRECTION", self.direction
-            d = {}
-            for child in childlist:
-                self._synchronize(obj, child, None, False)
-                key = self.mapper.instance_key(child)
-                #print "SYNCHRONIZED", child, "INSTANCE KEY", key
-                d[key] = child
-                uowcommit.unregister_object(child)
-
-            for child in childlist.added_items():
-                uowcommit.register_object(child)
-                key = self.mapper.instance_key(child)
-                #print "ADDED, INSTANCE KEY", key
-                d[key] = child
-
-            for child in childlist.unchanged_items():
-                key = self.mapper.instance_key(child)
-                o = d[key]
-                o._instance_key= key
+            # for the association mapper, the list of association objects is organized into a unique list based on the
+            # "primary key".  newly added association items which correspond to existing association items are "merged"
+            # into the existing one by moving the "_instance_key" over to the added item, so instead of insert/delete you
+            # just get an update operation.
+            if not delete:
+                tosave = util.OrderedDict()
+                for child in childlist:
+                    self._synchronize(obj, child, None, False)
+                    key = self.mapper.instance_key(child)
+                    tosave[key] = child
+                    uowcommit.unregister_object(child)
 
-            for child in childlist.deleted_items():
-                key = self.mapper.instance_key(child)
-                #print "DELETED, INSTANCE KEY", key
-                if d.has_key(key):
-                    o = d[key]
-                    o._instance_key = key
+                todelete = {}
+                for child in childlist.deleted_items():
+                    self._synchronize(obj, child, None, False)
+                    key = self.mapper.instance_key(child)
+                    if not tosave.has_key(key):
+                        todelete[key] = child
+                    else:
+                        tosave[key]._instance_key = key
                     uowcommit.unregister_object(child)
-                else:
-                    #print "DELETE ASSOC OBJ", repr(child)
-                    uowcommit.register_object(child, isdelete=True)
+                
+                for child in childlist.unchanged_items():
+                    key = self.mapper.instance_key(child)
+                    tosave[key]._instance_key = key
+                    
+                #print "OK for the save", [(o, getattr(o, '_instance_key', None)) for o in tosave.values()]
+                #print "OK for the delete", [(o, getattr(o, '_instance_key', None)) for o in todelete.values()]
+                
+                for obj in tosave.values():
+                    uowcommit.register_object(obj)
+                for obj in todelete.values():
+                    uowcommit.register_object(obj, isdelete=True)
+            else:
+                todelete = {}
+                for child in childlist.unchanged_items() + childlist.deleted_items():
+                    self._synchronize(obj, child, None, False)
+                    key = self.mapper.instance_key(child)
+                    todelete[key] = child
+                for obj in todelete.values():
+                    uowcommit.register_object(obj, isdelete=True)
+                    
+                
     def preprocess_dependencies(self, task, deplist, uowcommit, delete = False):
         # TODO: clean up the association step in process_dependencies and move the
         # appropriate sections of it to here
index 3e7e941f9691d1a2739d19171eba5e9b91e533d1..cdfbeb77c136356e4bb88854e6d9efb8f90c339c 100644 (file)
@@ -681,6 +681,7 @@ class Mapper(object):
         """called by a UnitOfWork object to delete objects, which involves a
         DELETE statement for each table used by this mapper, for each object in the list."""
         connection = uow.transaction.connection(self)
+        #print "DELETE_OBJ MAPPER", self.class_.__name__, objects
         
         for table in util.reversed(self.tables):
             if not self._has_pks(table):
index b697ab38290f92fc37fae5c543b70200f70bb3f2..e6d5c424e133eff6f308bbd961c7115791be4f42 100644 (file)
@@ -305,6 +305,7 @@ class UOWTransaction(object):
         if mod: self._mark_modified()
 
     def unregister_object(self, obj):
+        #print "UNREGISTER", obj
         mapper = object_mapper(obj)
         task = self.get_task_by_mapper(mapper)
         if obj in task.objects:
index ebe8535e1dc8cee7b78a80dc3ceeca866b128d93..0676ca1350df0027821d37ba71800c6ba563799f 100644 (file)
@@ -2,7 +2,7 @@
 """dumps out a string representation of a UOWTask structure"""
 
 class UOWDumper(object):
-    def __init__(self, task, buf, verbose=False):
+    def __init__(self, task, buf, verbose=True):
         self.verbose = verbose
         self.indent = 0
         self.task = task
index 8b9876d87d80f91fda752c8dfbe93beb71468454..b6c0d8fc5da9ff0709cc317e09c31f8c9108d5c3 100644 (file)
@@ -40,6 +40,7 @@ def suite():
         'objectstore',
         'cascade',
         'relationships',
+        'association',
         
         # cyclical ORM persistence
         'cycles',
@@ -49,7 +50,7 @@ def suite():
         'manytomany',
         'onetoone',
         'inheritance',
-       'polymorph',
+        'polymorph',
         
         # extensions
         'proxy_engine',
diff --git a/test/association.py b/test/association.py
new file mode 100644 (file)
index 0000000..e33151e
--- /dev/null
@@ -0,0 +1,143 @@
+import testbase
+
+from sqlalchemy import *
+
+
+class AssociationTest(testbase.PersistTest):
+    def setUpAll(self):
+        global items, item_keywords, keywords, metadata, Item, Keyword, KeywordAssociation
+        metadata = BoundMetaData(testbase.db)
+        items = Table('items', metadata, 
+            Column('item_id', Integer, primary_key=True),
+            Column('name', String(40)),
+            )
+        item_keywords = Table('item_keywords', metadata,
+            Column('item_id', Integer, ForeignKey('items.item_id')),
+            Column('keyword_id', Integer, ForeignKey('keywords.keyword_id')),
+            Column('data', String(40))
+            )
+        keywords = Table('keywords', metadata,
+            Column('keyword_id', Integer, primary_key=True),
+            Column('name', String(40))
+            )
+        metadata.create_all()
+        
+        class Item(object):
+            def __init__(self, name):
+                self.name = name
+            def __repr__(self):
+                return "Item id=%d name=%s keywordassoc=%s" % (self.item_id, self.name, repr(self.keywords))
+        class Keyword(object):
+            def __init__(self, name):
+                self.name = name
+            def __repr__(self):
+                return "Keyword id=%d name=%s" % (self.keyword_id, self.name)
+        class KeywordAssociation(object):
+            def __init__(self, keyword, data):
+                self.keyword = keyword
+                self.data = data
+            def __repr__(self):
+                return "KeywordAssociation itemid=%d keyword=%s data=%s" % (self.item_id, repr(self.keyword), self.data)
+        
+        mapper(Keyword, keywords)
+        mapper(KeywordAssociation, item_keywords, properties={
+            'keyword':relation(Keyword, lazy=False)
+        }, primary_key=[item_keywords.c.item_id, item_keywords.c.keyword_id], order_by=[item_keywords.c.data])
+        mapper(Item, items, properties={
+            'keywords' : relation(KeywordAssociation, association=Keyword)
+        })
+        
+    def tearDown(self):
+        for t in metadata.table_iterator(reverse=True):
+            t.delete().execute()
+    def tearDownAll(self):
+        clear_mappers()
+        metadata.drop_all()
+        
+    def testinsert(self):
+        sess = create_session()
+        item1 = Item('item1')
+        item2 = Item('item2')
+        item1.keywords.append(KeywordAssociation(Keyword('blue'), 'blue_assoc'))
+        item1.keywords.append(KeywordAssociation(Keyword('red'), 'red_assoc'))
+        item2.keywords.append(KeywordAssociation(Keyword('green'), 'green_assoc'))
+        sess.save(item1)
+        sess.save(item2)
+        sess.flush()
+        saved = repr([item1, item2])
+        sess.clear()
+        l = sess.query(Item).select()
+        loaded = repr(l)
+        print saved
+        print loaded
+        self.assert_(saved == loaded)
+
+    def testreplace(self):
+        sess = create_session()
+        item1 = Item('item1')
+        item1.keywords.append(KeywordAssociation(Keyword('blue'), 'blue_assoc'))
+        item1.keywords.append(KeywordAssociation(Keyword('red'), 'red_assoc'))
+        sess.save(item1)
+        sess.flush()
+        
+        red_keyword = item1.keywords[1].keyword
+        del item1.keywords[1]
+        item1.keywords.append(KeywordAssociation(red_keyword, 'new_red_assoc'))
+        sess.flush()
+        saved = repr([item1])
+        sess.clear()
+        l = sess.query(Item).select()
+        loaded = repr(l)
+        print saved
+        print loaded
+        self.assert_(saved == loaded)
+
+    def testmodify(self):
+        sess = create_session()
+        item1 = Item('item1')
+        item2 = Item('item2')
+        item1.keywords.append(KeywordAssociation(Keyword('blue'), 'blue_assoc'))
+        item1.keywords.append(KeywordAssociation(Keyword('red'), 'red_assoc'))
+        item2.keywords.append(KeywordAssociation(Keyword('green'), 'green_assoc'))
+        sess.save(item1)
+        sess.save(item2)
+        sess.flush()
+        
+        red_keyword = item1.keywords[1].keyword
+        del item1.keywords[0]
+        del item1.keywords[0]
+        purple_keyword = Keyword('purple')
+        item1.keywords.append(KeywordAssociation(red_keyword, 'new_red_assoc'))
+        item2.keywords.append(KeywordAssociation(purple_keyword, 'purple_item2_assoc'))
+        item1.keywords.append(KeywordAssociation(purple_keyword, 'purple_item1_assoc'))
+        item1.keywords.append(KeywordAssociation(Keyword('yellow'), 'yellow_assoc'))
+        
+        sess.flush()
+        saved = repr([item1, item2])
+        sess.clear()
+        l = sess.query(Item).select()
+        loaded = repr(l)
+        print saved
+        print loaded
+        self.assert_(saved == loaded)
+
+    def testdelete(self):
+        sess = create_session()
+        item1 = Item('item1')
+        item2 = Item('item2')
+        item1.keywords.append(KeywordAssociation(Keyword('blue'), 'blue_assoc'))
+        item1.keywords.append(KeywordAssociation(Keyword('red'), 'red_assoc'))
+        item2.keywords.append(KeywordAssociation(Keyword('green'), 'green_assoc'))
+        sess.save(item1)
+        sess.save(item2)
+        sess.flush()
+        self.assert_(item_keywords.count().scalar() == 3)
+
+        sess.delete(item1)
+        sess.delete(item2)
+        sess.flush()
+        self.assert_(item_keywords.count().scalar() == 0)
+
+        
+if __name__ == "__main__":
+    testbase.main()        
index d9a9d6e504990848655d0ab5022841322df85c27..4aac8a29093f262f8d730464a821add8bea18e49 100644 (file)
@@ -1,5 +1,3 @@
-"""Test complex relationships"""
-
 import testbase
 import unittest, sys, datetime