From decc3247b48d0cdfb70ed26a971cbaeb0079a0b4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 3 Dec 2005 04:34:12 +0000 Subject: [PATCH] added a third "mapper" to a many-to-many relationship that becomes the dependency in the "middle", thus allowing circular many-to-many relationships 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 | 87 +++++++++++++++++++++++++--------------- lib/sqlalchemy/schema.py | 2 + lib/sqlalchemy/sql.py | 13 ++++-- lib/sqlalchemy/util.py | 5 ++- test/double.py | 38 ++++++++++++++++++ 5 files changed, 109 insertions(+), 36 deletions(-) diff --git a/lib/sqlalchemy/mapper.py b/lib/sqlalchemy/mapper.py index 12b3d8e61a..7dba622204 100644 --- a/lib/sqlalchemy/mapper.py +++ b/lib/sqlalchemy/mapper.py @@ -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.""" diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 527d57023c..7206cf3c20 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -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) diff --git a/lib/sqlalchemy/sql.py b/lib/sqlalchemy/sql.py index 937bc90478..486404f33a 100644 --- a/lib/sqlalchemy/sql.py +++ b/lib/sqlalchemy/sql.py @@ -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 diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index 32a8efa41a..742fd9aa72 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -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)] diff --git a/test/double.py b/test/double.py index e82f767092..5c552700e9 100644 --- a/test/double.py +++ b/test/double.py @@ -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() -- 2.47.2