diff options
Diffstat (limited to 'lib/sqlalchemy/orm/strategies.py')
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 671 |
1 files changed, 346 insertions, 325 deletions
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 462954f6b..babd6e4c0 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -6,12 +6,11 @@ """sqlalchemy.orm.interfaces.LoaderStrategy implementations, and related MapperOptions.""" -from sqlalchemy import sql, schema, util, exceptions, sql_util, logging -from sqlalchemy.orm import mapper, query -from sqlalchemy.orm.interfaces import * +from sqlalchemy import sql, util, exceptions, sql_util, logging +from sqlalchemy.orm import mapper, attributes +from sqlalchemy.orm.interfaces import LoaderStrategy, StrategizedOption, MapperOption, PropertyOption from sqlalchemy.orm import session as sessionlib from sqlalchemy.orm import util as mapperutil -import random class ColumnLoader(LoaderStrategy): @@ -19,8 +18,9 @@ class ColumnLoader(LoaderStrategy): super(ColumnLoader, self).init() self.columns = self.parent_property.columns self._should_log_debug = logging.is_debug_enabled(self.logger) + self.is_composite = hasattr(self.parent_property, 'composite_class') - def setup_query(self, context, eagertable=None, parentclauses=None, **kwargs): + def setup_query(self, context, parentclauses=None, **kwargs): for c in self.columns: if parentclauses is not None: context.statement.append_column(parentclauses.aliased_column(c)) @@ -28,16 +28,93 @@ class ColumnLoader(LoaderStrategy): context.statement.append_column(c) def init_class_attribute(self): + if self.is_composite: + self._init_composite_attribute() + else: + self._init_scalar_attribute() + + def _init_composite_attribute(self): + self.logger.info("register managed composite attribute %s on class %s" % (self.key, self.parent.class_.__name__)) + def copy(obj): + return self.parent_property.composite_class(*obj.__colset__()) + def compare(a, b): + for col, aprop, bprop in zip(self.columns, a.__colset__(), b.__colset__()): + if not col.type.compare_values(aprop, bprop): + return False + else: + return True + sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, copy_function=copy, compare_function=compare, mutable_scalars=True, comparator=self.parent_property.comparator) + + def _init_scalar_attribute(self): self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__)) coltype = self.columns[0].type - sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, copy_function=coltype.copy_value, compare_function=coltype.compare_values, mutable_scalars=self.columns[0].type.is_mutable()) + sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, copy_function=coltype.copy_value, compare_function=coltype.compare_values, mutable_scalars=self.columns[0].type.is_mutable(), comparator=self.parent_property.comparator) + + def create_row_processor(self, selectcontext, mapper, row): + if self.is_composite: + for c in self.columns: + if c not in row: + break + else: + def execute(instance, row, isnew, ispostselect=None, **flags): + if isnew or ispostselect: + if self._should_log_debug: + self.logger.debug("populating %s with %s/%s..." % (mapperutil.attribute_str(instance, self.key), row.__class__.__name__, self.columns[0].key)) + instance.__dict__[self.key] = self.parent_property.composite_class(*[row[c] for c in self.columns]) + self.logger.debug("Returning active composite column fetcher for %s %s" % (mapper, self.key)) + return (execute, None) + + elif self.columns[0] in row: + def execute(instance, row, isnew, ispostselect=None, **flags): + if isnew or ispostselect: + if self._should_log_debug: + self.logger.debug("populating %s with %s/%s" % (mapperutil.attribute_str(instance, self.key), row.__class__.__name__, self.columns[0].key)) + instance.__dict__[self.key] = row[self.columns[0]] + self.logger.debug("Returning active column fetcher for %s %s" % (mapper, self.key)) + return (execute, None) + + (hosted_mapper, needs_tables) = selectcontext.attributes.get(('polymorphic_fetch', mapper), (None, None)) + if hosted_mapper is None: + return (None, None) + + if hosted_mapper.polymorphic_fetch == 'deferred': + def execute(instance, row, isnew, **flags): + if isnew: + sessionlib.attribute_manager.init_instance_attribute(instance, self.key, callable_=self._get_deferred_loader(instance, mapper, needs_tables)) + self.logger.debug("Returning deferred column fetcher for %s %s" % (mapper, self.key)) + return (execute, None) + else: + self.logger.debug("Returning no column fetcher for %s %s" % (mapper, self.key)) + return (None, None) + + def _get_deferred_loader(self, instance, mapper, needs_tables): + def load(): + group = [p for p in mapper.iterate_properties if isinstance(p.strategy, ColumnLoader) and p.columns[0].table in needs_tables] - def process_row(self, selectcontext, instance, row, identitykey, isnew): - if isnew: if self._should_log_debug: - self.logger.debug("populating %s with %s/%s" % (mapperutil.attribute_str(instance, self.key), row.__class__.__name__, self.columns[0].key)) - instance.__dict__[self.key] = row[self.columns[0]] - + self.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(instance, self.key), group and ','.join([p.key for p in group]) or 'None')) + + session = sessionlib.object_session(instance) + if session is None: + raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key)) + + cond, param_names = mapper._deferred_inheritance_condition(needs_tables) + statement = sql.select(needs_tables, cond, use_labels=True) + params = {} + for c in param_names: + params[c.name] = mapper.get_attr_by_column(instance, c) + + result = session.execute(statement, params, mapper=mapper) + try: + row = result.fetchone() + for prop in group: + sessionlib.attribute_manager.get_attribute(instance, prop.key).set_committed_value(instance, row[prop.columns[0]]) + return attributes.ATTR_WAS_SET + finally: + result.close() + + return load + ColumnLoader.logger = logging.class_logger(ColumnLoader) class DeferredColumnLoader(LoaderStrategy): @@ -47,74 +124,86 @@ class DeferredColumnLoader(LoaderStrategy): This is per-column lazy loading. """ + def create_row_processor(self, selectcontext, mapper, row): + if self.group is not None and selectcontext.attributes.get(('undefer', self.group), False): + return self.parent_property._get_strategy(ColumnLoader).create_row_processor(selectcontext, mapper, row) + elif not self.is_default or len(selectcontext.options): + def execute(instance, row, isnew, **flags): + if isnew: + if self._should_log_debug: + self.logger.debug("set deferred callable on %s" % mapperutil.attribute_str(instance, self.key)) + sessionlib.attribute_manager.init_instance_attribute(instance, self.key, callable_=self.setup_loader(instance)) + return (execute, None) + else: + def execute(instance, row, isnew, **flags): + if isnew: + if self._should_log_debug: + self.logger.debug("set deferred callable on %s" % mapperutil.attribute_str(instance, self.key)) + sessionlib.attribute_manager.reset_instance_attribute(instance, self.key) + return (execute, None) + def init(self): super(DeferredColumnLoader, self).init() + if hasattr(self.parent_property, 'composite_class'): + raise NotImplementedError("Deferred loading for composite types not implemented yet") self.columns = self.parent_property.columns self.group = self.parent_property.group self._should_log_debug = logging.is_debug_enabled(self.logger) def init_class_attribute(self): self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__)) - sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, callable_=lambda i:self.setup_loader(i), copy_function=lambda x: self.columns[0].type.copy_value(x), compare_function=lambda x,y:self.columns[0].type.compare_values(x,y), mutable_scalars=self.columns[0].type.is_mutable()) + sessionlib.attribute_manager.register_attribute(self.parent.class_, self.key, uselist=False, callable_=self.setup_loader, copy_function=self.columns[0].type.copy_value, compare_function=self.columns[0].type.compare_values, mutable_scalars=self.columns[0].type.is_mutable(), comparator=self.parent_property.comparator) def setup_query(self, context, **kwargs): - pass + if self.group is not None and context.attributes.get(('undefer', self.group), False): + self.parent_property._get_strategy(ColumnLoader).setup_query(context, **kwargs) - def process_row(self, selectcontext, instance, row, identitykey, isnew): - if isnew: - if not self.is_default or len(selectcontext.options): - sessionlib.attribute_manager.init_instance_attribute(instance, self.key, False, callable_=self.setup_loader(instance, selectcontext.options)) - else: - sessionlib.attribute_manager.reset_instance_attribute(instance, self.key) - - def setup_loader(self, instance, options=None): - if not mapper.has_mapper(instance): + def setup_loader(self, instance): + localparent = mapper.object_mapper(instance, raiseerror=False) + if localparent is None: return None - else: - prop = mapper.object_mapper(instance).props[self.key] - if prop is not self.parent_property: - return prop._get_strategy(DeferredColumnLoader).setup_loader(instance) - def lazyload(): - if self._should_log_debug: - self.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(instance, self.key), str(self.group))) + + prop = localparent.get_property(self.key) + if prop is not self.parent_property: + return prop._get_strategy(DeferredColumnLoader).setup_loader(instance) + def lazyload(): if not mapper.has_identity(instance): return None - try: - pk = self.parent.pks_by_table[self.columns[0].table] - except KeyError: - pk = self.columns[0].table.primary_key - - clause = sql.and_() - for primary_key in pk: - attr = self.parent.get_attr_by_column(instance, primary_key) - if not attr: - return None - clause.clauses.append(primary_key == attr) + if self.group is not None: + group = [p for p in localparent.iterate_properties if isinstance(p.strategy, DeferredColumnLoader) and p.group==self.group] + else: + group = None + + if self._should_log_debug: + self.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(instance, self.key), group and ','.join([p.key for p in group]) or 'None')) session = sessionlib.object_session(instance) if session is None: raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key)) - - localparent = mapper.object_mapper(instance) - if self.group is not None: - groupcols = [p for p in localparent.props.values() if isinstance(p.strategy, DeferredColumnLoader) and p.group==self.group] - result = session.execute(localparent, sql.select([g.columns[0] for g in groupcols], clause, use_labels=True), None) + + clause = localparent._get_clause + ident = instance._instance_key[1] + params = {} + for i, primary_key in enumerate(localparent.primary_key): + params[primary_key._label] = ident[i] + if group is not None: + statement = sql.select([p.columns[0] for p in group], clause, from_obj=[localparent.mapped_table], use_labels=True) + else: + statement = sql.select([self.columns[0]], clause, from_obj=[localparent.mapped_table], use_labels=True) + + if group is not None: + result = session.execute(statement, params, mapper=localparent) try: row = result.fetchone() - for prop in groupcols: - if prop is self: - continue - # set a scalar object instance directly on the object, - # bypassing SmartProperty event handlers. - sessionlib.attribute_manager.init_instance_attribute(instance, prop.key, uselist=False) - instance.__dict__[prop.key] = row[prop.columns[0]] - return row[self.columns[0]] + for prop in group: + sessionlib.attribute_manager.get_attribute(instance, prop.key).set_committed_value(instance, row[prop.columns[0]]) + return attributes.ATTR_WAS_SET finally: result.close() else: - return session.scalar(localparent, sql.select([self.columns[0]], clause, use_labels=True),None) + return session.scalar(sql.select([self.columns[0]], clause, from_obj=[localparent.mapped_table], use_labels=True),params, mapper=localparent) return lazyload @@ -131,6 +220,15 @@ class DeferredOption(StrategizedOption): else: return ColumnLoader +class UndeferGroupOption(MapperOption): + def __init__(self, group): + self.group = group + def process_query_context(self, context): + context.attributes[('undefer', self.group)] = True + + def process_selection_context(self, context): + context.attributes[('undefer', self.group)] = True + class AbstractRelationLoader(LoaderStrategy): def init(self): super(AbstractRelationLoader, self).init() @@ -139,22 +237,26 @@ class AbstractRelationLoader(LoaderStrategy): self._should_log_debug = logging.is_debug_enabled(self.logger) def _init_instance_attribute(self, instance, callable_=None): - return sessionlib.attribute_manager.init_instance_attribute(instance, self.key, self.uselist, cascade=self.cascade, trackparent=True, callable_=callable_) + return sessionlib.attribute_manager.init_instance_attribute(instance, self.key, callable_=callable_) def _register_attribute(self, class_, callable_=None): self.logger.info("register managed %s attribute %s on class %s" % ((self.uselist and "list-holding" or "scalar"), self.key, self.parent.class_.__name__)) - sessionlib.attribute_manager.register_attribute(class_, self.key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade, trackparent=True, typecallable=self.parent_property.collection_class, callable_=callable_) + sessionlib.attribute_manager.register_attribute(class_, self.key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade, trackparent=True, typecallable=self.parent_property.collection_class, callable_=callable_, comparator=self.parent_property.comparator) class NoLoader(AbstractRelationLoader): def init_class_attribute(self): self._register_attribute(self.parent.class_) - def process_row(self, selectcontext, instance, row, identitykey, isnew): - if isnew: - if not self.is_default or len(selectcontext.options): - if self._should_log_debug: - self.logger.debug("set instance-level no loader on %s" % mapperutil.attribute_str(instance, self.key)) - self._init_instance_attribute(instance) + def create_row_processor(self, selectcontext, mapper, row): + if not self.is_default or len(selectcontext.options): + def execute(instance, row, isnew, **flags): + if isnew: + if self._should_log_debug: + self.logger.debug("set instance-level no loader on %s" % mapperutil.attribute_str(instance, self.key)) + self._init_instance_attribute(instance) + return (execute, None) + else: + return (None, None) NoLoader.logger = logging.class_logger(NoLoader) @@ -167,7 +269,8 @@ class LazyLoader(AbstractRelationLoader): # determine if our "lazywhere" clause is the same as the mapper's # get() clause. then we can just use mapper.get() - self.use_get = not self.uselist and query.Query(self.mapper)._get_clause.compare(self.lazywhere) + #from sqlalchemy.orm import query + self.use_get = not self.uselist and self.mapper._get_clause.compare(self.lazywhere) if self.use_get: self.logger.info(str(self.parent_property) + " will use query.get() to optimize instance loads") @@ -178,7 +281,7 @@ class LazyLoader(AbstractRelationLoader): if not mapper.has_mapper(instance): return None else: - prop = mapper.object_mapper(instance).props[self.key] + prop = mapper.object_mapper(instance).get_property(self.key) if prop is not self.parent_property: return prop._get_strategy(LazyLoader).setup_loader(instance) def lazyload(): @@ -211,20 +314,27 @@ class LazyLoader(AbstractRelationLoader): # if we have a simple straight-primary key load, use mapper.get() # to possibly save a DB round trip + q = session.query(self.mapper) if self.use_get: ident = [] - for primary_key in self.select_mapper.pks_by_table[self.select_mapper.mapped_table]: + # TODO: when options are added to allow switching between union-based and non-union + # based polymorphic loads on a per-query basis, this code needs to switch between "mapper" and "select_mapper", + # probably via the query's own "mapper" property, and also use one of two "lazy" clauses, + # one against the "union" the other not + for primary_key in self.select_mapper.primary_key: bind = self.lazyreverse[primary_key] ident.append(params[bind.key]) - return session.query(self.mapper).get(ident) + return q.get(ident) elif self.order_by is not False: - order_by = self.order_by + q = q.order_by(self.order_by) elif self.secondary is not None and self.secondary.default_order_by() is not None: - order_by = self.secondary.default_order_by() - else: - order_by = False - result = session.query(self.mapper, with_options=options).select_whereclause(self.lazywhere, order_by=order_by, params=params) + q = q.order_by(self.secondary.default_order_by()) + if options: + q = q.options(*options) + q = q.filter(self.lazywhere).params(**params) + + result = q.all() if self.uselist: return result else: @@ -232,25 +342,37 @@ class LazyLoader(AbstractRelationLoader): return result[0] else: return None + + if self.uselist: + return q.all() + else: + return q.first() + return lazyload - def process_row(self, selectcontext, instance, row, identitykey, isnew): - if isnew: - # new object instance being loaded from a result row - if not self.is_default or len(selectcontext.options): - self.logger.debug("set instance-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key)) - # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader, - # which will override the clareset_instance_attributess-level behavior - self._init_instance_attribute(instance, callable_=self.setup_loader(instance, selectcontext.options)) - else: - self.logger.debug("set class-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key)) - # we are the primary manager for this attribute on this class - reset its per-instance attribute state, - # so that the class-level lazy loader is executed when next referenced on this instance. - # this usually is not needed unless the constructor of the object referenced the attribute before we got - # to load data into it. - sessionlib.attribute_manager.reset_instance_attribute(instance, self.key) - - def _create_lazy_clause(cls, prop, reverse_direction=False): + def create_row_processor(self, selectcontext, mapper, row): + if not self.is_default or len(selectcontext.options): + def execute(instance, row, isnew, **flags): + if isnew: + if self._should_log_debug: + self.logger.debug("set instance-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key)) + # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader, + # which will override the clareset_instance_attributess-level behavior + self._init_instance_attribute(instance, callable_=self.setup_loader(instance, selectcontext.options)) + return (execute, None) + else: + def execute(instance, row, isnew, **flags): + if isnew: + if self._should_log_debug: + self.logger.debug("set class-level lazy loader on %s" % mapperutil.attribute_str(instance, self.key)) + # we are the primary manager for this attribute on this class - reset its per-instance attribute state, + # so that the class-level lazy loader is executed when next referenced on this instance. + # this usually is not needed unless the constructor of the object referenced the attribute before we got + # to load data into it. + sessionlib.attribute_manager.reset_instance_attribute(instance, self.key) + return (execute, None) + + def _create_lazy_clause(cls, prop, reverse_direction=False, op='=='): (primaryjoin, secondaryjoin, remote_side) = (prop.polymorphic_primaryjoin, prop.polymorphic_secondaryjoin, prop.remote_side) binds = {} @@ -272,19 +394,16 @@ class LazyLoader(AbstractRelationLoader): FindColumnInColumnClause().traverse(expr) return len(columns) and columns[0] or None - def bind_label(): - # TODO: make this generation deterministic - return "lazy_" + hex(random.randint(0, 65535))[2:] - def visit_binary(binary): leftcol = find_column_in_expr(binary.left) rightcol = find_column_in_expr(binary.right) if leftcol is None or rightcol is None: return + if should_bind(leftcol, rightcol): col = leftcol binary.left = binds.setdefault(leftcol, - sql.bindparam(bind_label(), None, shortname=leftcol.name, type=binary.right.type, unique=True)) + sql.bindparam(None, None, shortname=leftcol.name, type_=binary.right.type, unique=True)) reverse[rightcol] = binds[col] # the "left is not right" compare is to handle part of a join clause that is "table.c.col1==table.c.col1", @@ -292,21 +411,19 @@ class LazyLoader(AbstractRelationLoader): if leftcol is not rightcol and should_bind(rightcol, leftcol): col = rightcol binary.right = binds.setdefault(rightcol, - sql.bindparam(bind_label(), None, shortname=rightcol.name, type=binary.left.type, unique=True)) + sql.bindparam(None, None, shortname=rightcol.name, type_=binary.left.type, unique=True)) reverse[leftcol] = binds[col] - lazywhere = primaryjoin.copy_container() + lazywhere = primaryjoin li = mapperutil.BinaryVisitor(visit_binary) if not secondaryjoin or not reverse_direction: - li.traverse(lazywhere) + lazywhere = li.traverse(lazywhere, clone=True) if secondaryjoin is not None: - secondaryjoin = secondaryjoin.copy_container() if reverse_direction: - li.traverse(secondaryjoin) + secondaryjoin = li.traverse(secondaryjoin, clone=True) lazywhere = sql.and_(lazywhere, secondaryjoin) - return (lazywhere, binds, reverse) _create_lazy_clause = classmethod(_create_lazy_clause) @@ -318,154 +435,42 @@ class EagerLoader(AbstractRelationLoader): def init(self): super(EagerLoader, self).init() - if self.parent.isa(self.mapper): - raise exceptions.ArgumentError( - "Error creating eager relationship '%s' on parent class '%s' " - "to child class '%s': Cant use eager loading on a self " - "referential relationship." % - (self.key, repr(self.parent.class_), repr(self.mapper.class_))) if self.is_default: self.parent._eager_loaders.add(self.parent_property) self.clauses = {} - self.clauses_by_lead_mapper = {} - - class AliasedClauses(object): - """Defines a set of join conditions and table aliases which - are aliased on a randomly-generated alias name, corresponding - to the connection of an optional parent AliasedClauses object - and a target mapper. - - EagerLoader has a distinct AliasedClauses object per parent - AliasedClauses object, so that all paths from one mapper to - another across a chain of eagerloaders generates a distinct - chain of joins. The AliasedClauses objects are generated and - cached on an as-needed basis. - - E.g.:: - - mapper A --> - (EagerLoader 'items') --> - mapper B --> - (EagerLoader 'keywords') --> - mapper C - - will generate:: - - EagerLoader 'items' --> { - None : AliasedClauses(items, None, alias_suffix='AB34') # mappera JOIN mapperb_AB34 - } - - EagerLoader 'keywords' --> [ - None : AliasedClauses(keywords, None, alias_suffix='43EF') # mapperb JOIN mapperc_43EF - AliasedClauses(items, None, alias_suffix='AB34') : - AliasedClauses(keywords, items, alias_suffix='8F44') # mapperb_AB34 JOIN mapperc_8F44 - ] - """ - - def __init__(self, eagerloader, parentclauses=None): - self.id = (parentclauses is not None and (parentclauses.id + "/") or '') + str(eagerloader.parent_property) - self.parent = eagerloader - self.target = eagerloader.select_table - self.eagertarget = eagerloader.select_table.alias(self._aliashash("/target")) - self.extra_cols = {} - - if eagerloader.secondary: - self.eagersecondary = eagerloader.secondary.alias(self._aliashash("/secondary")) - if parentclauses is not None: - aliasizer = sql_util.ClauseAdapter(self.eagertarget).\ - chain(sql_util.ClauseAdapter(self.eagersecondary)).\ - chain(sql_util.ClauseAdapter(parentclauses.eagertarget)) - else: - aliasizer = sql_util.ClauseAdapter(self.eagertarget).\ - chain(sql_util.ClauseAdapter(self.eagersecondary)) - self.eagersecondaryjoin = eagerloader.polymorphic_secondaryjoin.copy_container() - aliasizer.traverse(self.eagersecondaryjoin) - self.eagerprimary = eagerloader.polymorphic_primaryjoin.copy_container() - aliasizer.traverse(self.eagerprimary) - else: - self.eagerprimary = eagerloader.polymorphic_primaryjoin.copy_container() - if parentclauses is not None: - aliasizer = sql_util.ClauseAdapter(self.eagertarget) - aliasizer.chain(sql_util.ClauseAdapter(parentclauses.eagertarget, exclude=eagerloader.parent_property.remote_side)) - else: - aliasizer = sql_util.ClauseAdapter(self.eagertarget) - aliasizer.traverse(self.eagerprimary) - - if eagerloader.order_by: - self.eager_order_by = sql_util.ClauseAdapter(self.eagertarget).copy_and_process(util.to_list(eagerloader.order_by)) - else: - self.eager_order_by = None - - self._row_decorator = self._create_decorator_row() - - def aliased_column(self, column): - """return the aliased version of the given column, creating a new label for it if not already - present in this AliasedClauses eagertable.""" - - conv = self.eagertarget.corresponding_column(column, raiseerr=False) - if conv: - return conv - - if column in self.extra_cols: - return self.extra_cols[column] - - aliased_column = column.copy_container() - sql_util.ClauseAdapter(self.eagertarget).traverse(aliased_column) - alias = self._aliashash(column.name) - aliased_column = aliased_column.label(alias) - self._row_decorator.map[column] = alias - self.extra_cols[column] = aliased_column - return aliased_column - - def _aliashash(self, extra): - """return a deterministic 4 digit hash value for this AliasedClause's id + extra.""" - # use the first 4 digits of an MD5 hash - return "anon_" + util.hash(self.id + extra)[0:4] - - def _create_decorator_row(self): - class EagerRowAdapter(object): - def __init__(self, row): - self.row = row - def has_key(self, key): - return map.has_key(key) or self.row.has_key(key) - def __getitem__(self, key): - if map.has_key(key): - key = map[key] - return self.row[key] - def keys(self): - return map.keys() - map = {} - for c in self.eagertarget.c: - parent = self.target.corresponding_column(c) - map[parent] = c - map[parent._label] = c - map[parent.name] = c - EagerRowAdapter.map = map - return EagerRowAdapter - - def _decorate_row(self, row): - # adapts a row at row iteration time to transparently - # convert plain columns into the aliased columns that were actually - # added to the column clause of the SELECT. - return self._row_decorator(row) + self.join_depth = self.parent_property.join_depth def init_class_attribute(self): self.parent_property._get_strategy(LazyLoader).init_class_attribute() - def setup_query(self, context, eagertable=None, parentclauses=None, parentmapper=None, **kwargs): + def setup_query(self, context, parentclauses=None, parentmapper=None, **kwargs): """Add a left outer join to the statement thats being constructed.""" + # build a path as we setup the query. the format of this path + # matches that of interfaces.LoaderStack, and will be used in the + # row-loading phase to match up AliasedClause objects with the current + # LoaderStack position. + if parentclauses: + path = parentclauses.path + (self.parent.base_mapper(), self.key) + else: + path = (self.parent.base_mapper(), self.key) + + + if self.join_depth: + if len(path) / 2 > self.join_depth: + return + else: + if self.mapper in path: + return + + #print "CREATING EAGER PATH FOR", "->".join([str(s) for s in path]) + if parentmapper is None: localparent = context.mapper else: localparent = parentmapper - if self.mapper in context.recursion_stack: - return - else: - context.recursion_stack.add(self.parent) - statement = context.statement if hasattr(statement, '_outerjoin'): @@ -487,55 +492,57 @@ class EagerLoader(AbstractRelationLoader): break else: raise exceptions.InvalidRequestError("EagerLoader cannot locate a clause with which to outer join to, in query '%s' %s" % (str(statement), localparent.mapped_table)) - + try: - clauses = self.clauses[parentclauses] + clauses = self.clauses[path] except KeyError: - clauses = EagerLoader.AliasedClauses(self, parentclauses) - self.clauses[parentclauses] = clauses - - if context.mapper not in self.clauses_by_lead_mapper: - self.clauses_by_lead_mapper[context.mapper] = clauses - + clauses = mapperutil.PropertyAliasedClauses(self.parent_property, self.parent_property.polymorphic_primaryjoin, self.parent_property.polymorphic_secondaryjoin, parentclauses) + self.clauses[path] = clauses + if self.secondaryjoin is not None: - statement._outerjoin = sql.outerjoin(towrap, clauses.eagersecondary, clauses.eagerprimary).outerjoin(clauses.eagertarget, clauses.eagersecondaryjoin) + statement._outerjoin = sql.outerjoin(towrap, clauses.secondary, clauses.primaryjoin).outerjoin(clauses.alias, clauses.secondaryjoin) if self.order_by is False and self.secondary.default_order_by() is not None: - statement.order_by(*clauses.eagersecondary.default_order_by()) + statement.append_order_by(*clauses.secondary.default_order_by()) else: - statement._outerjoin = towrap.outerjoin(clauses.eagertarget, clauses.eagerprimary) - if self.order_by is False and clauses.eagertarget.default_order_by() is not None: - statement.order_by(*clauses.eagertarget.default_order_by()) + statement._outerjoin = towrap.outerjoin(clauses.alias, clauses.primaryjoin) + if self.order_by is False and clauses.alias.default_order_by() is not None: + statement.append_order_by(*clauses.alias.default_order_by()) - if clauses.eager_order_by: - statement.order_by(*util.to_list(clauses.eager_order_by)) - + if clauses.order_by: + statement.append_order_by(*util.to_list(clauses.order_by)) + statement.append_from(statement._outerjoin) - for value in self.select_mapper.props.values(): - value.setup(context, eagertable=clauses.eagertarget, parentclauses=clauses, parentmapper=self.select_mapper) - def _create_row_processor(self, selectcontext, row): - """Create a *row processing* function that will apply eager + for value in self.select_mapper.iterate_properties: + value.setup(context, parentclauses=clauses, parentmapper=self.select_mapper) + + def _create_row_decorator(self, selectcontext, row, path): + """Create a *row decorating* function that will apply eager aliasing to the row. Also check that an identity key can be retrieved from the row, else return None. """ + #print "creating row decorator for path ", "->".join([str(s) for s in path]) + # check for a user-defined decorator in the SelectContext (which was set up by the contains_eager() option) - if selectcontext.attributes.has_key((EagerLoader, self.parent_property)): + if selectcontext.attributes.has_key(("eager_row_processor", self.parent_property)): # custom row decoration function, placed in the selectcontext by the # contains_eager() mapper option - decorator = selectcontext.attributes[(EagerLoader, self.parent_property)] + decorator = selectcontext.attributes[("eager_row_processor", self.parent_property)] if decorator is None: decorator = lambda row: row else: try: # decorate the row according to the stored AliasedClauses for this eager load - clauses = self.clauses_by_lead_mapper[selectcontext.mapper] - decorator = clauses._row_decorator + clauses = self.clauses[path] + decorator = clauses.row_decorator except KeyError, k: # no stored AliasedClauses: eager loading was not set up in the query and # AliasedClauses never got initialized + if self._should_log_debug: + self.logger.debug("Could not locate aliased clauses for key: " + str(path)) return None try: @@ -550,81 +557,80 @@ class EagerLoader(AbstractRelationLoader): self.logger.debug("could not locate identity key from row '%s'; missing column '%s'" % (repr(decorated_row), str(k))) return None - def process_row(self, selectcontext, instance, row, identitykey, isnew): - """Receive a row. + def create_row_processor(self, selectcontext, mapper, row): + selectcontext.stack.push_property(self.key) + path = selectcontext.stack.snapshot() - Tell our mapper to look for a new object instance in the row, - and attach it to a list on the parent instance. - """ - - if self in selectcontext.recursion_stack: - return - - try: - # check for row processor - row_processor = selectcontext.attributes[id(self)] - except KeyError: - # create a row processor function and cache it in the context - row_processor = self._create_row_processor(selectcontext, row) - selectcontext.attributes[id(self)] = row_processor - - if row_processor is not None: - decorated_row = row_processor(row) - else: - # row_processor was None: degrade to a lazy loader - if self._should_log_debug: - self.logger.debug("degrade to lazy loader on %s" % mapperutil.attribute_str(instance, self.key)) - self.parent_property._get_strategy(LazyLoader).process_row(selectcontext, instance, row, identitykey, isnew) - return - - # TODO: recursion check a speed hit...? try to get a "termination point" into the AliasedClauses - # or EagerRowAdapter ? - selectcontext.recursion_stack.add(self) - try: - if not self.uselist: - if self._should_log_debug: - self.logger.debug("eagerload scalar instance on %s" % mapperutil.attribute_str(instance, self.key)) - if isnew: - # set a scalar object instance directly on the parent object, - # bypassing SmartProperty event handlers. - instance.__dict__[self.key] = self.mapper._instance(selectcontext, decorated_row, None) + row_decorator = self._create_row_decorator(selectcontext, row, path) + if row_decorator is not None: + def execute(instance, row, isnew, **flags): + decorated_row = row_decorator(row) + + selectcontext.stack.push_property(self.key) + + if not self.uselist: + if self._should_log_debug: + self.logger.debug("eagerload scalar instance on %s" % mapperutil.attribute_str(instance, self.key)) + if isnew: + # set a scalar object instance directly on the + # parent object, bypassing InstrumentedAttribute + # event handlers. + # + # FIXME: instead of... + sessionlib.attribute_manager.get_attribute(instance, self.key).set_raw_value(instance, self.mapper._instance(selectcontext, decorated_row, None)) + # bypass and set directly: + #instance.__dict__[self.key] = ... + else: + # call _instance on the row, even though the object has been created, + # so that we further descend into properties + self.mapper._instance(selectcontext, decorated_row, None) else: - # call _instance on the row, even though the object has been created, - # so that we further descend into properties - self.mapper._instance(selectcontext, decorated_row, None) - else: - if isnew: + if isnew: + if self._should_log_debug: + self.logger.debug("initialize UniqueAppender on %s" % mapperutil.attribute_str(instance, self.key)) + + collection = sessionlib.attribute_manager.init_collection(instance, self.key) + appender = util.UniqueAppender(collection, 'append_without_event') + + # store it in the "scratch" area, which is local to this load operation. + selectcontext.attributes[(instance, self.key)] = appender + result_list = selectcontext.attributes[(instance, self.key)] if self._should_log_debug: - self.logger.debug("initialize UniqueAppender on %s" % mapperutil.attribute_str(instance, self.key)) - # call the SmartProperty's initialize() method to create a new, blank list - l = getattr(instance.__class__, self.key).initialize(instance) - - # create an appender object which will add set-like semantics to the list - appender = util.UniqueAppender(l.data) - - # store it in the "scratch" area, which is local to this load operation. - selectcontext.attributes[(instance, self.key)] = appender - result_list = selectcontext.attributes[(instance, self.key)] - if self._should_log_debug: - self.logger.debug("eagerload list instance on %s" % mapperutil.attribute_str(instance, self.key)) - self.select_mapper._instance(selectcontext, decorated_row, result_list) - finally: - selectcontext.recursion_stack.remove(self) + self.logger.debug("eagerload list instance on %s" % mapperutil.attribute_str(instance, self.key)) + + self.select_mapper._instance(selectcontext, decorated_row, result_list) + selectcontext.stack.pop() + selectcontext.stack.pop() + return (execute, None) + else: + self.logger.debug("eager loader %s degrading to lazy loader" % str(self)) + selectcontext.stack.pop() + return self.parent_property._get_strategy(LazyLoader).create_row_processor(selectcontext, mapper, row) + + + def __str__(self): + return str(self.parent) + "." + self.key + EagerLoader.logger = logging.class_logger(EagerLoader) class EagerLazyOption(StrategizedOption): - def __init__(self, key, lazy=True): + def __init__(self, key, lazy=True, chained=False): super(EagerLazyOption, self).__init__(key) self.lazy = lazy - - def process_query_property(self, context, prop): + self.chained = chained + + def is_chained(self): + return not self.lazy and self.chained + + def process_query_property(self, context, properties): if self.lazy: - if prop in context.eager_loaders: - context.eager_loaders.remove(prop) + if properties[-1] in context.eager_loaders: + context.eager_loaders.remove(properties[-1]) else: - context.eager_loaders.add(prop) - super(EagerLazyOption, self).process_query_property(context, prop) + for prop in properties: + context.eager_loaders.add(prop) + super(EagerLazyOption, self).process_query_property(context, properties) def get_strategy_class(self): if self.lazy: @@ -636,24 +642,39 @@ class EagerLazyOption(StrategizedOption): EagerLazyOption.logger = logging.class_logger(EagerLazyOption) +# TODO: enable FetchMode option. currently +# this class does nothing. will require Query +# to swich between using its "polymorphic" selectable +# and its regular selectable in order to make decisions +# (therefore might require that FetchModeOperation is performed +# only as the first operation on a Query.) +class FetchModeOption(PropertyOption): + def __init__(self, key, type): + super(FetchModeOption, self).__init__(key) + if type not in ('join', 'select'): + raise exceptions.ArgumentError("Fetchmode must be one of 'join' or 'select'") + self.type = type + + def process_selection_property(self, context, properties): + context.attributes[('fetchmode', properties[-1])] = self.type + class RowDecorateOption(PropertyOption): def __init__(self, key, decorator=None, alias=None): super(RowDecorateOption, self).__init__(key) self.decorator = decorator self.alias = alias - def process_selection_property(self, context, property): + def process_selection_property(self, context, properties): if self.alias is not None and self.decorator is None: if isinstance(self.alias, basestring): - self.alias = property.target.alias(self.alias) + self.alias = properties[-1].target.alias(self.alias) def decorate(row): d = {} - for c in property.target.columns: + for c in properties[-1].target.columns: d[c] = row[self.alias.corresponding_column(c)] return d self.decorator = decorate - context.attributes[(EagerLoader, property)] = self.decorator + context.attributes[("eager_row_processor", properties[-1])] = self.decorator RowDecorateOption.logger = logging.class_logger(RowDecorateOption) - |