]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
added a third "mapper" to a many-to-many relationship that becomes the dependency...
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 3 Dec 2005 04:34:12 +0000 (04:34 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 3 Dec 2005 04:34:12 +0000 (04:34 +0000)
added testcase to the 'double' test suite (whose name will change...)
small fix to table.get_col_by_original
added **kwargs to EagerLazyOption so other property options can be sent through

lib/sqlalchemy/mapper.py
lib/sqlalchemy/schema.py
lib/sqlalchemy/sql.py
lib/sqlalchemy/util.py
test/double.py

index 12b3d8e61acce31f30f944230116ffcbb3e1e4ae..7dba6222047367033432e6025a70c0b57b99c5ea 100644 (file)
@@ -56,11 +56,6 @@ def _relation_mapper(class_, table=None, secondary=None,
     return _relation_loader(mapper(class_, table, **kwargs), secondary, primaryjoin, secondaryjoin, 
                     foreignkey=foreignkey, uselist=uselist, private=private, live=live, association=association, lazy=lazy, selectalias=selectalias)
 
-#def _relation_mapper(class_, table=None, secondary=None, 
-#                    primaryjoin=None, secondaryjoin=None, foreignkey=None, 
-#                    uselist=None, private=False, live=False, association=None, **kwargs):
-#    return _relation_loader(mapper(class_, table, **kwargs), secondary, primaryjoin=primaryjoin, secondaryjoin=secondaryjoin, foreignkey=foreignkey, uselist=uselist, private=private, live=live, association=association)
-
 class assignmapper(object):
     """provides a property object that will instantiate a Mapper for a given class the first
     time it is called off of the object.  This is useful for attaching a Mapper to a class
@@ -115,20 +110,20 @@ def extension(ext):
     """returns a MapperOption that will add the given MapperExtension to the 
     mapper returned by mapper.options()."""
     return ExtensionOption(ext)
-def eagerload(name):
+def eagerload(name, **kwargs):
     """returns a MapperOption that will convert the property of the given name
     into an eager load.  Used with mapper.options()"""
-    return EagerLazyOption(name, toeager=True)
+    return EagerLazyOption(name, toeager=True, **kwargs)
 
-def lazyload(name):
+def lazyload(name, **kwargs):
     """returns a MapperOption that will convert the property of the given name
     into a lazy load.  Used with mapper.options()"""
-    return EagerLazyOption(name, toeager=False)
+    return EagerLazyOption(name, toeager=False, **kwargs)
 
-def noload(name):
+def noload(name, **kwargs):
     """returns a MapperOption that will convert the property of the given name
     into a non-load.  Used with mapper.options()"""
-    return EagerLazyOption(name, toeager=None)
+    return EagerLazyOption(name, toeager=None, **kwargs)
     
 def object_mapper(object):
     """given an object, returns the primary Mapper associated with the object
@@ -169,7 +164,7 @@ class Mapper(object):
             'table':table,
             'primarytable':primarytable,
             'scope':scope,
-            'properties':properties,
+            'properties':properties or {},
             'primary_keys':primary_keys,
             'is_primary':False,
             'inherits':inherits,
@@ -286,6 +281,7 @@ class Mapper(object):
     engines = property(lambda s: [t.engine for t in s.tables])
 
     def add_property(self, key, prop):
+        self.copyargs['properties'][key] = prop
         if isinstance(prop, schema.Column):
             self.columns[key] = prop
             prop = ColumnProperty(prop)
@@ -759,11 +755,9 @@ class PropertyLoader(MapperProperty):
 
         if self.uselist is None:
             self.uselist = True
-                    
+
         self._compile_synchronizers()
                 
-        #if not hasattr(parent.class_, key):
-            #print "regiser list col on class %s key %s" % (parent.class_.__name__, key)
         if self._is_primary():
             self._set_class_attribute(parent.class_, key)
     
@@ -880,21 +874,45 @@ class PropertyLoader(MapperProperty):
             if child is not None:
                 uow.register_deleted(child)
 
-            
+    class MapperStub(object):
+      """poses as a Mapper representing the association table in a many-to-many
+      join, when performing a commit().  
+      
+      The Task objects in the objectstore module treat it just like
+      any other Mapper, but in fact it only serves as a "dependency" placeholder
+      for the many-to-many update task."""
+      def save_obj(self, *args, **kwargs):
+        pass
+      def delete_obj(self, *args, **kwargs):
+        pass
+        
     def register_dependencies(self, uowcommit):
+        """tells a UOWTransaction what mappers are dependent on which, with regards
+        to the two or three mappers handled by this PropertyLoader.
+        
+        Also registers itself as a "processor" for one of its mappers, which
+        will be executed after that mapper's objects have been saved or before
+        they've been deleted.  The process operation manages attributes and dependent
+        operations upon the objects of one of the involved mappers."""
         if self.association is not None:
+            # association object.  our mapper is made to be dependent on our parent,
+            # as well as the object we associate to.  when theyre done saving (or before they
+            # are deleted), we will process child items off objects managed by our parent mapper.
             uowcommit.register_dependency(self.parent, self.mapper)
-            uowcommit.register_dependency(self.association, self.parent)
+            uowcommit.register_dependency(self.association, self.mapper)
             uowcommit.register_processor(self.parent, self, self.parent, False)
             uowcommit.register_processor(self.parent, self, self.parent, True)
         elif self.direction == PropertyLoader.CENTER:
-            # with many-to-many, set the parent as dependent on us, then the 
-            # list of associations as dependent on the parent
-            # if only a list changes, the parent mapper is the only mapper that
-            # gets added to the "todo" list
-            uowcommit.register_dependency(self.mapper, self.parent)
-            uowcommit.register_processor(self.parent, self, self.parent, False)
-            uowcommit.register_processor(self.parent, self, self.parent, True)
+            # many-to-many.  create a "Stub" mapper to represent the
+            # "middle table" in the relationship.  This stub mapper doesnt save
+            # or delete any objects, but just marks a dependency on the two
+            # related mappers.  its dependency processor then populates the
+            # association table.
+            stub = PropertyLoader.MapperStub()
+            uowcommit.register_dependency(self.parent, stub)
+            uowcommit.register_dependency(self.mapper, stub)
+            uowcommit.register_processor(stub, self, self.parent, False)
+            uowcommit.register_processor(stub, self, self.parent, True)
         elif self.direction == PropertyLoader.LEFT:
             uowcommit.register_dependency(self.parent, self.mapper)
             uowcommit.register_processor(self.parent, self, self.parent, False)
@@ -974,8 +992,7 @@ class PropertyLoader(MapperProperty):
                     uowcommit.register_object(child)
                 uowcommit.register_deleted_list(childlist)
         elif self.association is not None:
-            # TODO: this is new code, for managing "association objects".
-            # its probably glitchy.
+            # manage association objects.
             for obj in deplist:
                 childlist = getlist(obj, passive=True)
                 if childlist is None: continue
@@ -1020,10 +1037,6 @@ class PropertyLoader(MapperProperty):
                 if self.direction != PropertyLoader.RIGHT or len(childlist.added_items()) == 0:
                     for child in childlist.deleted_items():
                         if not self.private:
-                            # TODO: we arent sync'ing if this child object
-                            # is to be deleted.  this is because if its an "association"
-                            # object, it needs its data in order to be located.  
-                            # need more explicit support for "association" objects.
                             self._synchronize(obj, child, None, True)
                         if self.direction == PropertyLoader.LEFT:
                             uowcommit.register_object(child, isdelete=self.private)
@@ -1257,9 +1270,10 @@ class ExtensionOption(MapperOption):
         
 class EagerLazyOption(MapperOption):
     """an option that switches a PropertyLoader to be an EagerLoader or LazyLoader"""
-    def __init__(self, key, toeager = True):
+    def __init__(self, key, toeager = True, **kwargs):
         self.key = key
         self.toeager = toeager
+        self.kwargs = kwargs
 
     def hash_key(self):
         return "EagerLazyOption(%s, %s)" % (repr(self.key), repr(self.toeager))
@@ -1282,7 +1296,16 @@ class EagerLazyOption(MapperOption):
             class_ = PropertyLoader
         else:
             class_ = LazyLoader
-        mapper.set_property(key, class_(submapper, oldprop.secondary, primaryjoin = oldprop.primaryjoin, secondaryjoin = oldprop.secondaryjoin, foreignkey=oldprop.foreignkey, uselist=oldprop.uselist, private=oldprop.private, live=oldprop.live, isoption=True ))
+            
+        self.kwargs.setdefault('primaryjoin', oldprop.primaryjoin)
+        self.kwargs.setdefault('secondaryjoin', oldprop.secondaryjoin)
+        self.kwargs.setdefault('foreignkey', oldprop.foreignkey)
+        self.kwargs.setdefault('uselist', oldprop.uselist)
+        self.kwargs.setdefault('private', oldprop.private)
+        self.kwargs.setdefault('live', oldprop.live)
+        self.kwargs.setdefault('selectalias', oldprop.selectalias)
+        self.kwargs['isoption'] = True
+        mapper.set_property(key, class_(submapper, oldprop.secondary, **self.kwargs ))
 
 class Aliasizer(sql.ClauseVisitor):
     """converts a table instance within an expression to be an alias of that table."""
index 527d57023c6ed85ce60d3a05a196795592b5a4ef..7206cf3c20948ba429c6d2f50e8fb4c8a1029f43 100644 (file)
@@ -270,6 +270,8 @@ class ForeignKey(SchemaItem):
         visitor.visit_foreign_key(self)
         
     def _set_parent(self, column):
+        if not isinstance(column, Column):
+          raise "hi" + repr(type(column))
         self.parent = column
         self.parent.foreign_key = self
         self.parent.table.foreign_keys.append(self)
index 937bc90478553b1011599fe30e44ab720ff83f5f..486404f33a9bb4ad849fc73817908b1ff4702fdb 100644 (file)
@@ -256,7 +256,7 @@ class ClauseElement(object):
 
         Note that since ClauseElements may be mutable, the hash_key() value is subject to
         change if the underlying structure of the ClauseElement changes.""" 
-       raise NotImplementedError(repr(self))
+        raise NotImplementedError(repr(self))
     def _get_from_objects(self):
         raise NotImplementedError(repr(self))
     def _process_from_dict(self, data, asfrom):
@@ -772,7 +772,14 @@ class TableImpl(Selectable):
     engine = property(lambda s: s.table.engine)
 
     def get_col_by_original(self, column):
-        return self.columns.get(column.key, None)
+        try:
+          col = self.columns[column.key]
+        except KeyError:
+          return None
+        if col.original is column:
+          return col
+        else:
+          return None
 
     def group_parenthesized(self):
         return False
@@ -1048,7 +1055,7 @@ class UpdateBase(ClauseElement):
             else:
                 try:
                     d[self.table.columns[str(key)]] = value
-                except AttributeError:
+                except KeyError:
                     pass
 
         # create a list of column assignment clauses as tuples
index 32a8efa41ada62ac3a9f0f061c98681813f6f59b..742fd9aa722fb55d073c5adbb0cdb4efc832d0af 100644 (file)
@@ -36,7 +36,10 @@ class OrderedProperties(object):
     def __setitem__(self, key, object):
         setattr(self, key, object)
     def __getitem__(self, key):
-        return getattr(self, key)
+        try:
+          return getattr(self, key)
+        except AttributeError:
+          raise KeyError(key)
     def __delitem__(self, key):
         delattr(self, key)
         del self._list[self._list.index(key)]
index e82f767092cae38d5d01dd0bfa3e5711840b6781..5c552700e996a02efa56294ecf8e3b390cf39ed8 100644 (file)
@@ -86,6 +86,44 @@ class DoubleTest(testbase.AssertMixin):
             }
             )    
 
+    def testcircular(self):
+        """tests a circular many-to-many relationship.  this requires that the mapper
+        "break off" a new "mapper stub" to indicate a third depedendent processor."""
+        Place.mapper = mapper(Place, place)
+        Transition.mapper = mapper(Transition, transition, properties = dict(
+            inputs = relation(Place.mapper, place_output, lazy=True),
+            outputs = relation(Place.mapper, place_input, lazy=True),
+            )
+        )
+        Place.mapper.add_property('inputs', relation(Transition.mapper, place_output, lazy=True))
+        Place.mapper.add_property('outputs', relation(Transition.mapper, place_input, lazy=True))
+        
+
+        t1 = Transition('transition1')
+        t2 = Transition('transition2')
+        t3 = Transition('transition3')
+        p1 = Place('place1')
+        p2 = Place('place2')
+        p3 = Place('place3')
+
+        t1.inputs.append(p1)
+        t1.inputs.append(p2)
+        t1.outputs.append(p3)
+        t2.inputs.append(p1)
+        p2.inputs.append(t2)
+        p3.inputs.append(t2)
+        p1.outputs.append(t1)
+        
+        objectstore.commit()
+
+        Place.eagermapper = Place.mapper.options(
+            eagerload('inputs', selectalias='ip_alias'), 
+            eagerload('outputs', selectalias='op_alias')
+        )
+        
+        l = Place.eagermapper.select()
+        print repr(l)
+
 if __name__ == "__main__":    
     testbase.main()