From: Mike Bayer Date: Thu, 9 Feb 2006 00:33:26 +0000 (+0000) Subject: deprecated "selectalias" argument on eager loader, do use_alias=True X-Git-Tag: rel_0_1_0~40 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3051a6957369ede59e51461c89953a9973e25708;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git deprecated "selectalias" argument on eager loader, do use_alias=True "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__. --- diff --git a/lib/sqlalchemy/mapping/__init__.py b/lib/sqlalchemy/mapping/__init__.py index f208a932ec..f3a2ac5094 100644 --- a/lib/sqlalchemy/mapping/__init__.py +++ b/lib/sqlalchemy/mapping/__init__.py @@ -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) diff --git a/lib/sqlalchemy/mapping/mapper.py b/lib/sqlalchemy/mapping/mapper.py index 597214220d..8fa1703536 100644 --- a/lib/sqlalchemy/mapping/mapper.py +++ b/lib/sqlalchemy/mapping/mapper.py @@ -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()])) - ) - ) diff --git a/lib/sqlalchemy/mapping/objectstore.py b/lib/sqlalchemy/mapping/objectstore.py index 8d0a64f6ca..d10396bc08 100644 --- a/lib/sqlalchemy/mapping/objectstore.py +++ b/lib/sqlalchemy/mapping/objectstore.py @@ -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: diff --git a/lib/sqlalchemy/mapping/properties.py b/lib/sqlalchemy/mapping/properties.py index 72cb447496..61bb3da369 100644 --- a/lib/sqlalchemy/mapping/properties.py +++ b/lib/sqlalchemy/mapping/properties.py @@ -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):