]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
deprecated "selectalias" argument on eager loader, do use_alias=True
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 9 Feb 2006 00:33:26 +0000 (00:33 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 9 Feb 2006 00:33:26 +0000 (00:33 +0000)
"eager alias" flag will propigate to child eager loaders so the full query comes
out OK.  mappers/properties have overhauled "copy" methodology.  mappers
are no longer "singleton" and no longer have elaborate "hash_key" methods - there
is a primary mapper associated with a class which is done via direct dictionary
relationship, and the options() method on mapper does its own lighter-weight caching
of created mappers.  the unitofwork does extra work with the mappers it receives
to insure its dealing with "the primary" mapper, so that properties can be more liberal
about which mapper they reference (i.e. not the primary one).
options() works better but still could use a looking-at to see that
its not wasteful.  simplified mapper() method in __init__.

lib/sqlalchemy/mapping/__init__.py
lib/sqlalchemy/mapping/mapper.py
lib/sqlalchemy/mapping/objectstore.py
lib/sqlalchemy/mapping/properties.py

index f208a932ec87b2d2d21d9cc483045696ecb81c71..f3a2ac5094b64ebc2d84424a66939ae38d832de7 100644 (file)
@@ -45,23 +45,12 @@ def column(*columns, **kwargs):
 def deferred(*columns, **kwargs):
     return DeferredColumnProperty(*columns, **kwargs)
     
-def mapper(class_, table = None, engine = None, autoload = False, *args, **params):
+def mapper(class_, table=None, *args, **params):
     """returns a new or already cached Mapper object."""
     if table is None:
         return class_mapper(class_)
 
-    if isinstance(table, str):
-        table = schema.Table(table, engine, autoload = autoload, mustexist = not autoload)
-            
-    hashkey = mapper_hash_key(class_, table, *args, **params)
-    #print "HASHKEY: " + hashkey
-    try:
-        return mapper_registry[hashkey]
-    except KeyError:
-        m = Mapper(hashkey, class_, table, *args, **params)
-        mapper_registry.setdefault(hashkey, m)
-        m._init_properties()
-        return mapper_registry[hashkey]
+    return Mapper(class_, table, *args, **params)
 
 def clear_mappers():
     """removes all mappers that have been created thus far.  when new mappers are 
@@ -109,15 +98,15 @@ def object_mapper(object):
 
 def class_mapper(class_):
     """given a class, returns the primary Mapper associated with the class."""
+    return mapper_registry[class_]
     try:
-        return mapper_registry[class_._mapper]
+        return mapper_registry[class_]
     except KeyError:
         pass
     except AttributeError:
         pass
-        raise "Class '%s' has no mapper associated with it" % class_.__name__
+    raise "Class '%s' has no mapper associated with it" % class_.__name__
 
-mapperlib.class_mapper = class_mapper
 
 def assign_mapper(class_, *args, **params):
     params.setdefault("is_primary", True)
index 597214220dc4d7e2c3d189ac766715916055dba2..8fa17035364f712e31f208ed0c55233b7a2dc830 100644 (file)
@@ -11,15 +11,16 @@ import sqlalchemy.engine as engine
 import sqlalchemy.util as util
 import objectstore
 import sys
+import weakref
 
-mapper_registry = {}
+# a dictionary mapping classes to their primary mappers
+mapper_registry = weakref.WeakKeyDictionary()
 
 class Mapper(object):
     """Persists object instances to and from schema.Table objects via the sql package.
     Instances of this class should be constructed through this package's mapper() or
     relation() function."""
     def __init__(self, 
-                hashkey, 
                 class_, 
                 table, 
                 primarytable = None, 
@@ -33,18 +34,6 @@ class Mapper(object):
                 allow_column_override = False,
                 **kwargs):
 
-        self.copyargs = {
-            'class_':class_,
-            'table':table,
-            'properties':properties or {},
-            'primary_key':primary_key,
-            'is_primary':None,
-            'inherits':inherits,
-            'inherit_condition':inherit_condition,
-            'extension':extension,
-            'order_by':order_by
-        }
-
         if primarytable is not None:
             sys.stderr.write("'primarytable' argument to mapper is deprecated\n")
             
@@ -52,10 +41,10 @@ class Mapper(object):
             self.extension = MapperExtension()
         else:
             self.extension = extension                
-        self.hashkey = hashkey
         self.class_ = class_
         self.is_primary = is_primary
         self.order_by = order_by
+        self._options = {}
         
         if not issubclass(class_, object):
             raise TypeError("Class '%s' is not a new-style class" % class_.__name__)
@@ -167,26 +156,27 @@ class Mapper(object):
         for primary_key in self.pks_by_table[self.table]:
             self._get_clause.clauses.append(primary_key == sql.bindparam("pk_"+primary_key.key))
 
-        if (
-                (not hasattr(self.class_, '_mapper') or not mapper_registry.has_key(self.class_._mapper))
-                or self.is_primary 
-                or (inherits is not None and inherits._is_primary_mapper())
-            ):
+        if not mapper_registry.has_key(self.class_) or self.is_primary or (inherits is not None and inherits._is_primary_mapper()):
             objectstore.global_attributes.reset_class_managed(self.class_)
             self._init_class()
             self.identitytable = self.primarytable
         else:
-            self.identitytable = class_mapper(self.class_).table
+            self.identitytable = mapper_registry[self.class_].primarytable
                 
         if inherits is not None:
             for key, prop in inherits.props.iteritems():
                 if not self.props.has_key(key):
                     self.props[key] = prop._copy()
+                    self.props[key].parent = self
+                    self.props[key].key = None  # force re-init
+
+        for key, prop in self.props.iteritems():
+            if getattr(prop, 'key', None) is None:
+                prop.init(key, self)
 
     engines = property(lambda s: [t.engine for t in s.tables])
 
     def add_property(self, key, prop):
-        self.copyargs['properties'][key] = prop
         if sql.is_column(prop):
             self.columns[key] = prop
             prop = ColumnProperty(prop)
@@ -197,19 +187,15 @@ class Mapper(object):
                 proplist.append(prop)
         prop.init(key, self)
         
-    def _init_properties(self):
-        for key, prop in self.props.iteritems():
-            if getattr(prop, 'key', None) is None:
-                prop.init(key, self)
-        
     def __str__(self):
         return "Mapper|" + self.class_.__name__ + "|" + self.primarytable.name
         
-    def hash_key(self):
-        return self.hashkey
-
+    
     def _is_primary_mapper(self):
-        return getattr(self.class_, '_mapper', None) == self.hashkey
+        return mapper_registry.get(self.class_, None) is self
+
+    def _primary_mapper(self):
+        return mapper_registry[self.class_]
         
     def _init_class(self):
         """sets up our classes' overridden __init__ method, this mappers hash key as its
@@ -228,7 +214,7 @@ class Mapper(object):
                 if not nohist:
                     objectstore.uow().register_new(self)
             self.class_.__init__ = init
-        self.class_._mapper = self.hashkey
+        mapper_registry[self.class_] = self
         self.class_.c = self.c
         
     def set_property(self, key, prop):
@@ -302,30 +288,25 @@ class Mapper(object):
         compiling or executing it"""
         return self._compile(whereclause, **options)
 
-    def copy(self, hashkey=None):
-        # TODO: at the moment, we are re-using the properties from the original mapper
-        # which stay connected to that first mapper.  if we start making copies of 
-        # mappers where the primary attributes of the mapper change, we might want 
-        # to look into copying all the property objects too.
-        if hashkey is None:
-            hashkey = hash_key(self) + "->copy" 
-        mapper = Mapper(hashkey, **self.copyargs)
-        mapper._init_properties()
+    def copy(self):
+        mapper = Mapper.__new__(Mapper)
+        mapper.__dict__.update(self.__dict__)
+        mapper.props = self.props.copy()
         return mapper
         
     def options(self, *options):
         """uses this mapper as a prototype for a new mapper with different behavior.
         *options is a list of options directives, which include eagerload(), lazyload(), and noload()"""
 
-        hashkey = hash_key(self) + "->" + repr([hash_key(o) for o in options])
+        optkey = repr([hash_key(o) for o in options])
         try:
-            return mapper_registry[hashkey]
+            return self._options[optkey]
         except KeyError:
-            mapper = self.copy(hashkey)
-
+            mapper = self.copy()
             for option in options:
                 option.process(mapper)
-            return mapper_registry.setdefault(hashkey, mapper)
+            self._options[optkey] = mapper
+            return mapper
 
     def get_by(self, *args, **params):
         """returns a single object instance based on the given key/value criterion. 
@@ -509,7 +490,7 @@ class Mapper(object):
             # for this table, in the case that the user
             # specified custom primary key cols.
             for obj in objects:
-                #print "SAVE_OBJ we are " + hash_key(self) + " obj: " +  obj.__class__.__name__ + repr(id(obj))
+                #print "SAVE_OBJ we are Mapper(" + str(id(self)) + ") obj: " +  obj.__class__.__name__ + repr(id(obj))
                 params = {}
 
                 isinsert = not hasattr(obj, "_instance_key")
@@ -577,7 +558,7 @@ class Mapper(object):
                     if primary_key is not None:
                         i = 0
                         for col in self.pks_by_table[table]:
-                    #        print "col: " + table.name + "." + col.key + " val: " + repr(self._getattrbycolumn(obj, col))
+                            #print "col: " + table.name + "." + col.key + " val: " + repr(self._getattrbycolumn(obj, col))
                             if self._getattrbycolumn(obj, col) is None:
                                 self._setattrbycolumn(obj, col, primary_key[i])
                             i+=1
@@ -724,8 +705,6 @@ class Mapper(object):
             instance = self.extension.create_instance(self, row, imap, self.class_)
             if instance is None:
                 instance = self.class_(_mapper_nohistory=True)
-            # attach mapper hashkey to the instance ?
-            #instance._mapper = self.hashkey
             instance._instance_key = identitykey
 
             imap[identitykey] = instance
@@ -880,17 +859,6 @@ def hash_key(obj):
     else:
         return repr(obj)
     
-def mapper_hash_key(class_, table, primarytable = None, properties = None, **kwargs):
-    if properties is None:
-        properties = {}
-    return (
-        "Mapper(%s, %s, primarytable=%s, properties=%s)" % (
-            repr(class_),
-            hash_key(table),
-            hash_key(primarytable),
-            repr(dict([(k, hash_key(p)) for k,p in properties.iteritems()]))
-        )
-    )
 
 
 
index 8d0a64f6caa98397f4a9f6686814a5230199d7ec..d10396bc08792734537c78437c9b9c9630a8f169 100644 (file)
@@ -404,7 +404,9 @@ class UOWTransaction(object):
     def register_dependency(self, mapper, dependency):
         """called by mapper.PropertyLoader to register the objects handled by
         one mapper being dependent on the objects handled by another."""
-        self.dependencies[(mapper, dependency)] = True
+
+        # correct for primary mapper (the mapper offcially associated with the class)
+        self.dependencies[(mapper._primary_mapper(), dependency._primary_mapper())] = True
         self.__modified = True
 
     def register_processor(self, mapper, processor, mapperfrom, isdeletefrom):
@@ -416,6 +418,10 @@ class UOWTransaction(object):
         # to "mapperfrom"'s list of save/delete objects, and send them to "processor"
         # for dependency processing
         #print "registerprocessor", str(mapper), repr(processor.key), str(mapperfrom), repr(isdeletefrom)
+        
+        # correct for primary mapper (the mapper offcially associated with the class)
+        mapper = mapper._primary_mapper()
+        mapperfrom = mapperfrom._primary_mapper()
         task = self.get_task_by_mapper(mapper)
         targettask = self.get_task_by_mapper(mapperfrom)
         task.dependencies.append(UOWDependencyProcessor(processor, targettask, isdeletefrom))
@@ -683,7 +689,7 @@ class UOWTask(object):
                 # create a placeholder UOWTask that may be built into the final
                 # task tree
                 get_object_task(task, obj)
-                for dep in deps_by_targettask[task]:
+                for dep in deps_by_targettask.get(task, []):
                     (processor, targettask, isdelete) = (dep.processor, dep.targettask, dep.isdeletefrom)
                     if taskelement.isdelete is not dep.isdeletefrom:
                         continue
@@ -732,7 +738,7 @@ class UOWTask(object):
         if head is None:
             return None
 
-        #print str(head)
+        print str(head)
 
         def make_task_tree(node, parenttask):
             """takes a dependency-sorted tree of objects and creates a tree of UOWTasks"""
@@ -815,7 +821,7 @@ class UOWTask(object):
         def _repr_task(task):
             if task.mapper is not None:
                 if task.mapper.__class__.__name__ == 'Mapper':
-                    name = task.mapper.class_.__name__ + "/" + task.mapper.primarytable.id
+                    name = task.mapper.class_.__name__ + "/" + task.mapper.primarytable.id + "/" + str(id(task.mapper))
                 else:
                     name = repr(task.mapper)
             else:
index 72cb447496a230e41690be4d17277a0ade265a1b..61bb3da36905385b1e2ee01d9a0422345a926d57 100644 (file)
@@ -28,8 +28,6 @@ class ColumnProperty(MapperProperty):
         setattr(object, self.key, value)
     def get_history(self, obj, passive=False):
         return objectstore.global_attributes.get_history(obj, self.key, passive=passive)
-    def hash_key(self):
-        return "ColumnProperty(%s)" % repr([hash_key(c) for c in self.columns])
 
     def _copy(self):
         return ColumnProperty(*self.columns)
@@ -60,8 +58,6 @@ class DeferredColumnProperty(ColumnProperty):
         self.group = kwargs.get('group', None)
         ColumnProperty.__init__(self, *columns)
     
-    def hash_key(self):
-        return "DeferredColumnProperty(%s)" % repr([hash_key(c) for c in self.columns])
 
     def _copy(self):
         return DeferredColumnProperty(*self.columns)
@@ -125,7 +121,7 @@ class PropertyLoader(MapperProperty):
 
     """describes an object property that holds a single item or list of items that correspond
     to a related database table."""
-    def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, live=False, association=None, selectalias=None, order_by=False, attributeext=None, backref=None, is_backref=False):
+    def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, live=False, association=None, use_alias=False, selectalias=None, order_by=False, attributeext=None, backref=None, is_backref=False):
         self.uselist = uselist
         self.argument = argument
         self.secondary = secondary
@@ -135,19 +131,24 @@ class PropertyLoader(MapperProperty):
         self.private = private
         self.live = live
         self.association = association
-        self.selectalias = selectalias
+        if isinstance(selectalias, str):
+            print "'selectalias' argument to property is deprecated.  please use 'use_alias=True'"
+            self.use_alias = True
+        else:
+            self.use_alias = use_alias
         self.order_by = order_by
         self.attributeext=attributeext
         self.backref = backref
         self.is_backref = is_backref
-        self._hash_key = "%s(%s, %s, %s, %s, %s, %s, %s, %s)" % (self.__class__.__name__, hash_key(self.argument), hash_key(secondary), hash_key(primaryjoin), hash_key(secondaryjoin), hash_key(foreignkey), repr(uselist), repr(private), hash_key(self.order_by))
 
     def _copy(self):
-        return self.__class__(self.mapper, self.secondary, self.primaryjoin, self.secondaryjoin, self.foreignkey, self.uselist, self.private)
+        x = self.__class__.__new__(self.__class__)
+        x.__dict__.update(self.__dict__)
+        return x
+        
+    def init_subclass(self, key, parent):
+        pass
         
-    def hash_key(self):
-        return self._hash_key
-
     def init(self, key, parent):
         import sqlalchemy.mapping
         if isinstance(self.argument, type):
@@ -212,6 +213,8 @@ class PropertyLoader(MapperProperty):
         elif not objectstore.global_attributes.is_class_managed(parent.class_, key):
             raise "Non-primary property created for attribute '%s' on class '%s', but that attribute is not managed! Insure that the primary mapper for this class defines this property" % (key, parent.class_.__name__)
 
+        self.init_subclass(key, parent)
+        
     def _is_primary(self):
         """a return value of True indicates we are the primary PropertyLoader for this loader's
         attribute on our mapper's class.  It means we can set the object's attribute behavior
@@ -354,6 +357,8 @@ class PropertyLoader(MapperProperty):
             pass
         def delete_obj(self, *args, **kwargs):
             pass
+        def _primary_mapper(self):
+            return self
         
     def register_dependencies(self, uowcommit):
         """tells a UOWTransaction what mappers are dependent on which, with regards
@@ -569,8 +574,7 @@ class PropertyLoader(MapperProperty):
         objectstore.global_attributes.create_history(instance, self.key, self.uselist)
 
 class LazyLoader(PropertyLoader):
-    def init(self, key, parent):
-        PropertyLoader.init(self, key, parent)
+    def init_subclass(self, key, parent):
         (self.lazywhere, self.lazybinds) = create_lazy_clause(self.parent.table, self.primaryjoin, self.secondaryjoin, self.foreignkey)
         # determine if our "lazywhere" clause is the same as the mapper's
         # get() clause.  then we can just use mapper.get()
@@ -658,10 +662,15 @@ def create_lazy_clause(table, primaryjoin, secondaryjoin, foreignkey):
 
 class EagerLoader(PropertyLoader):
     """loads related objects inline with a parent query."""
-    def init(self, key, parent):
-        PropertyLoader.init(self, key, parent)
-        
+    def init_subclass(self, key, parent, recursion_stack=None):
         parent._has_eager = True
+
+        if recursion_stack is None:
+            recursion_stack = {}
+
+        if self.use_alias:
+            pass
+
         # figure out tables in the various join clauses we have, because user-defined
         # whereclauses that reference the same tables will be converted to use
         # aliases of those tables
@@ -670,7 +679,6 @@ class EagerLoader(PropertyLoader):
         if self.secondaryjoin is not None:
             [self.to_alias.append(f) for f in self.secondaryjoin._get_from_objects()]
         try:
-#            del self.to_alias[parent.primarytable]
             del self.to_alias[parent.table]
         except KeyError:
             pass
@@ -680,8 +688,8 @@ class EagerLoader(PropertyLoader):
         # or primary join condition to reference the aliased table (and the order_by).  
         # else we set up the target clause objects as what they are defined in the 
         # superclass.
-        if self.selectalias is not None:
-            self.eagertarget = self.target.alias(self.selectalias)
+        if self.use_alias:
+            self.eagertarget = self.target.alias()
             aliasizer = Aliasizer(self.target, aliases={self.target:self.eagertarget})
             if self.secondaryjoin is not None:
                 self.eagersecondary = self.secondaryjoin.copy_container()
@@ -700,6 +708,36 @@ class EagerLoader(PropertyLoader):
                         self.eager_order_by[i].accept_visitor(aliasizer)
             else:
                 self.eager_order_by = self.order_by
+
+            # we have to propigate the "use_alias" fact into 
+            # any sub-mappers that are also eagerloading so that they create a unique tablename
+            # as well.  this copies our child mapper and replaces any eager properties on the 
+            # new mapper with an equivalent eager property, just containing use_alias=True
+            eagerprops = []
+            for key, prop in self.mapper.props.iteritems():
+                if isinstance(prop, EagerLoader) and not prop.use_alias:
+                    eagerprops.append(prop)
+            if len(eagerprops):
+                recursion_stack[self] = True
+                self.mapper = self.mapper.copy()
+                try:
+                    for prop in eagerprops:
+                        p = prop._copy()
+                        p.use_alias=True
+
+                        self.mapper.props[prop.key] = p
+
+                        if recursion_stack.has_key(prop):
+                            raise "Circular eager load relationship detected on " + str(self.mapper) + " " + key + repr(self.mapper.props)
+
+                        p.init_subclass(prop.key, prop.parent, recursion_stack)
+
+                        p.eagerprimary = p.eagerprimary.copy_container()
+                        aliasizer = Aliasizer(p.parent.table, aliases={p.parent.table:self.eagertarget})
+                        p.eagerprimary.accept_visitor(aliasizer)
+                finally:
+                    del recursion_stack[self]
+
         else:
             self.eagertarget = self.target
             self.eagerprimary = self.primaryjoin
@@ -726,19 +764,12 @@ class EagerLoader(PropertyLoader):
         else:
             towrap = self.parent.table
 
-        if eagertable is not None:
-            eagerprimary = self.eagerprimary.copy_container()
-            aliasizer = Aliasizer(self.parent.table, aliases={self.parent.table:eagertable})
-            eagerprimary.accept_visitor(aliasizer)
-        else:
-            eagerprimary = self.eagerprimary
-                
         if self.secondaryjoin is not None:
-            statement._outerjoin = sql.outerjoin(towrap, self.secondary, eagerprimary).outerjoin(self.eagertarget, self.eagersecondary)
+            statement._outerjoin = sql.outerjoin(towrap, self.secondary, self.eagerprimary).outerjoin(self.eagertarget, self.eagersecondary)
             if self.order_by is False and self.secondary.default_order_by() is not None:
                 statement.order_by(*self.secondary.default_order_by())
         else:
-            statement._outerjoin = towrap.outerjoin(self.eagertarget, eagerprimary)
+            statement._outerjoin = towrap.outerjoin(self.eagertarget, self.eagerprimary)
             if self.order_by is False and self.eagertarget.default_order_by() is not None:
                 statement.order_by(*self.eagertarget.default_order_by())
 
@@ -746,7 +777,6 @@ class EagerLoader(PropertyLoader):
             statement.order_by(*util.to_list(self.eager_order_by))
             
         statement.append_from(statement._outerjoin)
-        #statement.append_column(self.eagertarget)
         recursion_stack[self] = True
         try:
             for key, value in self.mapper.props.iteritems():
@@ -778,11 +808,11 @@ class EagerLoader(PropertyLoader):
 
     def _instance(self, row, imap, result_list=None):
         """gets an instance from a row, via this EagerLoader's mapper."""
-        # if we have an alias for our mapper's table via the selectalias
+        # if we have an alias for our mapper's table via the use_alias
         # parameter, we need to translate the 
         # aliased columns from the incoming row into a new row that maps
         # the values against the columns of the mapper's original non-aliased table.
-        if self.selectalias is not None:
+        if self.use_alias:
             fakerow = {}
             fakerow = util.DictDecorator(row)
             for c in self.eagertarget.c:
@@ -802,9 +832,8 @@ class GenericOption(MapperOption):
         tokens = key.split('.', 1)
         if len(tokens) > 1:
             oldprop = mapper.props[tokens[0]]
-            kwargs = util.constructor_args(oldprop)
-            kwargs['argument'] = self.process_by_key(oldprop.mapper.copy(), tokens[1])
-            newprop = oldprop.__class__(**kwargs)
+            newprop = oldprop._copy()
+            newprop.argument = self.process_by_key(oldprop.mapper.copy(), tokens[1])
             mapper.set_property(tokens[0], newprop)
         else:
             self.create_prop(mapper, tokens[0])
@@ -832,10 +861,15 @@ class EagerLazyOption(GenericOption):
         else:
             class_ = LazyLoader
 
-        # create a clone of the class using mostly the arguments from the original
-        submapper = mapper.props[key].mapper
-        kwargs = util.constructor_args(mapper.props[key], **self.kwargs)
-        mapper.set_property(key, class_(**kwargs ))
+        oldprop = mapper.props[key]
+        newprop = class_.__new__(class_)
+        newprop.__dict__.update(oldprop.__dict__)
+        newprop.init_subclass(key, mapper)
+        if self.kwargs.get('selectalias', None):
+            newprop.use_alias = True
+        elif self.kwargs.get('use_alias', None) is not None:
+            newprop.use_alias = self.kwargs['use_alias']
+        mapper.set_property(key, newprop)
 
 class DeferredOption(GenericOption):
     def __init__(self, key, defer=False, **kwargs):