diff options
Diffstat (limited to 'lib/sqlalchemy/orm/strategies.py')
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 656 |
1 files changed, 338 insertions, 318 deletions
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 65a8b019b..8ae3042a6 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -6,11 +6,13 @@ """sqlalchemy.orm.interfaces.LoaderStrategy implementations, and related MapperOptions.""" -from sqlalchemy import sql, util, exceptions, logging +import sqlalchemy.exceptions as sa_exc +from sqlalchemy import sql, util, log from sqlalchemy.sql import util as sql_util from sqlalchemy.sql import visitors, expression, operators from sqlalchemy.orm import mapper, attributes -from sqlalchemy.orm.interfaces import LoaderStrategy, StrategizedOption, MapperOption, PropertyOption, serialize_path, deserialize_path +from sqlalchemy.orm.interfaces import LoaderStrategy, StrategizedOption, \ + MapperOption, PropertyOption, serialize_path, deserialize_path from sqlalchemy.orm import session as sessionlib from sqlalchemy.orm import util as mapperutil @@ -21,28 +23,53 @@ class ColumnLoader(LoaderStrategy): def init(self): super(ColumnLoader, self).init() self.columns = self.parent_property.columns - self._should_log_debug = logging.is_debug_enabled(self.logger) + self._should_log_debug = log.is_debug_enabled(self.logger) self.is_composite = hasattr(self.parent_property, 'composite_class') - def setup_query(self, context, parentclauses=None, **kwargs): + def setup_query(self, context, entity, path, adapter, column_collection=None, **kwargs): for c in self.columns: - if parentclauses is not None: - context.secondary_columns.append(parentclauses.aliased_column(c)) - else: - context.primary_columns.append(c) + if adapter: + c = adapter.columns[c] + column_collection.append(c) def init_class_attribute(self): self.is_class_level = True - if self.is_composite: - self._init_composite_attribute() + self.logger.info("%s register managed attribute" % self) + coltype = self.columns[0].type + sessionlib.register_attribute(self.parent.class_, self.key, uselist=False, useobject=False, copy_function=coltype.copy_value, compare_function=coltype.compare_values, mutable_scalars=self.columns[0].type.is_mutable(), comparator=self.parent_property.comparator, parententity=self.parent) + + def create_row_processor(self, selectcontext, path, mapper, row, adapter): + key, col = self.key, self.columns[0] + if adapter: + col = adapter.columns[col] + if col in row: + def new_execute(state, row, **flags): + state.dict[key] = row[col] + + if self._should_log_debug: + new_execute = self.debug_callable(new_execute, self.logger, + "%s returning active column fetcher" % self, + lambda state, row, **flags: "%s populating %s" % (self, mapperutil.state_attribute_str(state, key)) + ) + return (new_execute, None) else: - self._init_scalar_attribute() + def new_execute(state, row, isnew, **flags): + if isnew: + state.expire_attributes([key]) + if self._should_log_debug: + self.logger.debug("%s deferring load" % self) + return (new_execute, None) + +ColumnLoader.logger = log.class_logger(ColumnLoader) + +class CompositeColumnLoader(ColumnLoader): + def init_class_attribute(self): + self.is_class_level = True + self.logger.info("%s register managed composite attribute" % self) - 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.__composite_values__()) + return self.parent_property.composite_class(*obj.__composite_values__()) + def compare(a, b): for col, aprop, bprop in zip(self.columns, a.__composite_values__(), @@ -51,63 +78,56 @@ class ColumnLoader(LoaderStrategy): return False else: return True - sessionlib.register_attribute(self.parent.class_, self.key, uselist=False, useobject=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.register_attribute(self.parent.class_, self.key, uselist=False, useobject=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 new_execute(instance, row, **flags): - 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]) - if self._should_log_debug: - self.logger.debug("Returning active composite column fetcher for %s %s" % (mapper, self.key)) - return (new_execute, None, None) - - elif self.columns[0] in row: - def new_execute(instance, row, **flags): + sessionlib.register_attribute(self.parent.class_, self.key, uselist=False, useobject=False, copy_function=copy, compare_function=compare, mutable_scalars=True, comparator=self.parent_property.comparator, parententity=self.parent) + + def create_row_processor(self, selectcontext, path, mapper, row, adapter): + key, columns, composite_class = self.key, self.columns, self.parent_property.composite_class + if adapter: + columns = [adapter.columns[c] for c in columns] + for c in columns: + if c not in row: + def new_execute(state, row, isnew, **flags): + if isnew: + state.expire_attributes([key]) 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]] - if self._should_log_debug: - self.logger.debug("Returning active column fetcher for %s %s" % (mapper, self.key)) - return (new_execute, None, None) + self.logger.debug("%s deferring load" % self) + return (new_execute, None) else: - def new_execute(instance, row, isnew, **flags): - if isnew: - instance._state.expire_attributes([self.key]) + def new_execute(state, row, **flags): + state.dict[key] = composite_class(*[row[c] for c in columns]) + if self._should_log_debug: - self.logger.debug("Deferring load for %s %s" % (mapper, self.key)) - return (new_execute, None, None) + new_execute = self.debug_callable(new_execute, self.logger, + "%s returning active composite column fetcher" % self, + lambda state, row, **flags: "populating %s" % (mapperutil.state_attribute_str(state, key)) + ) -ColumnLoader.logger = logging.class_logger(ColumnLoader) + return (new_execute, None) +CompositeColumnLoader.logger = log.class_logger(CompositeColumnLoader) + class DeferredColumnLoader(LoaderStrategy): """Deferred column loader, a per-column or per-column-group lazy loader.""" - def create_row_processor(self, selectcontext, mapper, row): - if self.columns[0] in row: - return self.parent_property._get_strategy(ColumnLoader).create_row_processor(selectcontext, mapper, row) + def create_row_processor(self, selectcontext, path, mapper, row, adapter): + col = self.columns[0] + if adapter: + col = adapter.columns[col] + if col in row: + return self.parent_property._get_strategy(ColumnLoader).create_row_processor(selectcontext, path, mapper, row, adapter) + elif not self.is_class_level or len(selectcontext.options): - def new_execute(instance, row, **flags): - if self._should_log_debug: - self.logger.debug("set deferred callable on %s" % mapperutil.attribute_str(instance, self.key)) - instance._state.set_callable(self.key, self.setup_loader(instance)) - return (new_execute, None, None) + def new_execute(state, row, **flags): + state.set_callable(self.key, self.setup_loader(state)) else: - def new_execute(instance, row, **flags): - if self._should_log_debug: - self.logger.debug("set deferred callable on %s" % mapperutil.attribute_str(instance, self.key)) - instance._state.reset(self.key) - return (new_execute, None, None) + def new_execute(state, row, **flags): + state.reset(self.key) + + if self._should_log_debug: + new_execute = self.debug_callable(new_execute, self.logger, None, + lambda state, row, **flags: "set deferred callable on %s" % mapperutil.state_attribute_str(state, self.key) + ) + return (new_execute, None) def init(self): super(DeferredColumnLoader, self).init() @@ -115,25 +135,25 @@ class DeferredColumnLoader(LoaderStrategy): 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) + self._should_log_debug = log.is_debug_enabled(self.logger) def init_class_attribute(self): self.is_class_level = True - self.logger.info("register managed attribute %s on class %s" % (self.key, self.parent.class_.__name__)) - sessionlib.register_attribute(self.parent.class_, self.key, uselist=False, useobject=False, callable_=self.class_level_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) + self.logger.info("%s register managed attribute" % self) + sessionlib.register_attribute(self.parent.class_, self.key, uselist=False, useobject=False, callable_=self.class_level_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, parententity=self.parent) - def setup_query(self, context, only_load_props=None, **kwargs): + def setup_query(self, context, entity, path, adapter, only_load_props=None, **kwargs): if \ (self.group is not None and context.attributes.get(('undefer', self.group), False)) or \ (only_load_props and self.key in only_load_props): - self.parent_property._get_strategy(ColumnLoader).setup_query(context, **kwargs) + self.parent_property._get_strategy(ColumnLoader).setup_query(context, entity, path, adapter, **kwargs) - def class_level_loader(self, instance, props=None): - if not mapper.has_mapper(instance): + def class_level_loader(self, state, props=None): + if not mapperutil._state_has_mapper(state): return None - localparent = mapper.object_mapper(instance) + localparent = mapper._state_mapper(state) # adjust for the ColumnProperty associated with the instance # not being our own ColumnProperty. This can occur when entity_name @@ -141,38 +161,38 @@ class DeferredColumnLoader(LoaderStrategy): # to the class. prop = localparent.get_property(self.key) if prop is not self.parent_property: - return prop._get_strategy(DeferredColumnLoader).setup_loader(instance) + return prop._get_strategy(DeferredColumnLoader).setup_loader(state) - return LoadDeferredColumns(instance, self.key, props) + return LoadDeferredColumns(state, self.key, props) - def setup_loader(self, instance, props=None, create_statement=None): - return LoadDeferredColumns(instance, self.key, props, optimizing_statement=create_statement) + def setup_loader(self, state, props=None, create_statement=None): + return LoadDeferredColumns(state, self.key, props) -DeferredColumnLoader.logger = logging.class_logger(DeferredColumnLoader) +DeferredColumnLoader.logger = log.class_logger(DeferredColumnLoader) class LoadDeferredColumns(object): - """callable, serializable loader object used by DeferredColumnLoader""" + """serializable loader object used by DeferredColumnLoader""" - def __init__(self, instance, key, keys, optimizing_statement=None): - self.instance = instance + def __init__(self, state, key, keys): + self.state = state self.key = key self.keys = keys - self.optimizing_statement = optimizing_statement def __getstate__(self): - return {'instance':self.instance, 'key':self.key, 'keys':self.keys} + return {'state':self.state, 'key':self.key, 'keys':self.keys} def __setstate__(self, state): - self.instance = state['instance'] + self.state = state['state'] self.key = state['key'] self.keys = state['keys'] - self.optimizing_statement = None def __call__(self): - if not mapper.has_identity(self.instance): + state = self.state + + if not mapper._state_has_identity(state): return None - - localparent = mapper.object_mapper(self.instance, raiseerror=False) + + localparent = mapper._state_mapper(state) prop = localparent.get_property(self.key) strategy = prop._get_strategy(DeferredColumnLoader) @@ -185,22 +205,18 @@ class LoadDeferredColumns(object): toload = [self.key] # narrow the keys down to just those which have no history - group = [k for k in toload if k in self.instance._state.unmodified] + group = [k for k in toload if k in state.unmodified] if strategy._should_log_debug: - strategy.logger.debug("deferred load %s group %s" % (mapperutil.attribute_str(self.instance, self.key), group and ','.join(group) or 'None')) + strategy.logger.debug("deferred load %s group %s" % (mapperutil.state_attribute_str(state, self.key), group and ','.join(group) or 'None')) - session = sessionlib.object_session(self.instance) + session = sessionlib._state_session(state) if session is None: - raise exceptions.UnboundExecutionError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (self.instance.__class__, self.key)) + raise sa_exc.UnboundExecutionError("Parent instance %s is not bound to a Session; deferred load operation of attribute '%s' cannot proceed" % (mapperutil.state_str(state), self.key)) query = session.query(localparent) - if not self.optimizing_statement: - ident = self.instance._instance_key[1] - query._get(None, ident=ident, only_load_props=group, refresh_instance=self.instance._state) - else: - statement, params = self.optimizing_statement(self.instance) - query.from_statement(statement).params(params)._get(None, only_load_props=group, refresh_instance=self.instance._state) + ident = state.key[1] + query._get(None, ident=ident, only_load_props=group, refresh_instance=state) return attributes.ATTR_WAS_SET class DeferredOption(StrategizedOption): @@ -223,55 +239,63 @@ class UndeferGroupOption(MapperOption): class AbstractRelationLoader(LoaderStrategy): def init(self): super(AbstractRelationLoader, self).init() - for attr in ['primaryjoin', 'secondaryjoin', 'secondary', 'foreign_keys', 'mapper', 'target', 'table', 'uselist', 'cascade', 'attributeext', 'order_by', 'remote_side', 'direction']: + for attr in ['mapper', 'target', 'table', 'uselist']: setattr(self, attr, getattr(self.parent_property, attr)) - self._should_log_debug = logging.is_debug_enabled(self.logger) + self._should_log_debug = log.is_debug_enabled(self.logger) - def _init_instance_attribute(self, instance, callable_=None): + def _init_instance_attribute(self, state, callable_=None): if callable_: - instance._state.set_callable(self.key, callable_) + state.set_callable(self.key, callable_) else: - instance._state.initialize(self.key) + state.initialize(self.key) def _register_attribute(self, class_, callable_=None, **kwargs): - 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.register_attribute(class_, self.key, uselist=self.uselist, useobject=True, extension=self.attributeext, cascade=self.cascade, trackparent=True, typecallable=self.parent_property.collection_class, callable_=callable_, comparator=self.parent_property.comparator, **kwargs) + self.logger.info("%s register managed %s attribute" % (self, (self.uselist and "collection" or "scalar"))) + + if self.parent_property.backref: + attribute_ext = self.parent_property.backref.extension + else: + attribute_ext = None + + sessionlib.register_attribute(class_, self.key, uselist=self.uselist, useobject=True, extension=attribute_ext, cascade=self.parent_property.cascade, trackparent=True, typecallable=self.parent_property.collection_class, callable_=callable_, comparator=self.parent_property.comparator, parententity=self.parent, **kwargs) class NoLoader(AbstractRelationLoader): def init_class_attribute(self): self.is_class_level = True self._register_attribute(self.parent.class_) - def create_row_processor(self, selectcontext, mapper, row): - def new_execute(instance, row, ispostselect, **flags): - if not ispostselect: - if self._should_log_debug: - self.logger.debug("initializing blank scalar/collection on %s" % mapperutil.attribute_str(instance, self.key)) - self._init_instance_attribute(instance) - return (new_execute, None, None) + def create_row_processor(self, selectcontext, path, mapper, row, adapter): + def new_execute(state, row, **flags): + self._init_instance_attribute(state) + + if self._should_log_debug: + new_execute = self.debug_callable(new_execute, self.logger, None, + lambda state, row, **flags: "initializing blank scalar/collection on %s" % mapperutil.state_attribute_str(state, self.key) + ) + return (new_execute, None) -NoLoader.logger = logging.class_logger(NoLoader) +NoLoader.logger = log.class_logger(NoLoader) class LazyLoader(AbstractRelationLoader): def init(self): super(LazyLoader, self).init() (self.__lazywhere, self.__bind_to_col, self._equated_columns) = self.__create_lazy_clause(self.parent_property) - self.logger.info(str(self.parent_property) + " lazy loading clause " + str(self.__lazywhere)) + self.logger.info("%s lazy loading clause %s" % (self, self.__lazywhere)) # determine if our "lazywhere" clause is the same as the mapper's # get() clause. then we can just use mapper.get() #from sqlalchemy.orm import query self.use_get = not self.uselist and self.mapper._get_clause[0].compare(self.__lazywhere) if self.use_get: - self.logger.info(str(self.parent_property) + " will use query.get() to optimize instance loads") + self.logger.info("%s will use query.get() to optimize instance loads" % self) def init_class_attribute(self): self.is_class_level = True self._register_attribute(self.parent.class_, callable_=self.class_level_loader) - def lazy_clause(self, instance, reverse_direction=False): - if instance is None: + def lazy_clause(self, state, reverse_direction=False): + if state is None: return self._lazy_none_clause(reverse_direction) if not reverse_direction: @@ -285,8 +309,8 @@ class LazyLoader(AbstractRelationLoader): # use the "committed" (database) version to get query column values # also its a deferred value; so that when used by Query, the committed value is used # after an autoflush occurs - bindparam.value = lambda: mapper._get_committed_attr_by_column(instance, bind_to_col[bindparam.key]) - return visitors.traverse(criterion, clone=True, visit_bindparam=visit_bindparam) + bindparam.value = lambda: mapper._get_committed_state_attr_by_column(state, bind_to_col[bindparam.key]) + return visitors.cloned_traverse(criterion, {}, {'bindparam':visit_bindparam}) def _lazy_none_clause(self, reverse_direction=False): if not reverse_direction: @@ -305,13 +329,13 @@ class LazyLoader(AbstractRelationLoader): binary.right = expression.null() binary.operator = operators.is_ - return visitors.traverse(criterion, clone=True, visit_binary=visit_binary) + return visitors.cloned_traverse(criterion, {}, {'binary':visit_binary}) - def class_level_loader(self, instance, options=None, path=None): - if not mapper.has_mapper(instance): + def class_level_loader(self, state, options=None, path=None): + if not mapperutil._state_has_mapper(state): return None - localparent = mapper.object_mapper(instance) + localparent = mapper._state_mapper(state) # adjust for the PropertyLoader associated with the instance # not being our own PropertyLoader. This can occur when entity_name @@ -319,35 +343,41 @@ class LazyLoader(AbstractRelationLoader): # to the class. prop = localparent.get_property(self.key) if prop is not self.parent_property: - return prop._get_strategy(LazyLoader).setup_loader(instance) + return prop._get_strategy(LazyLoader).setup_loader(state) - return LoadLazyAttribute(instance, self.key, options, path) + return LoadLazyAttribute(state, self.key, options, path) - def setup_loader(self, instance, options=None, path=None): - return LoadLazyAttribute(instance, self.key, options, path) + def setup_loader(self, state, options=None, path=None): + return LoadLazyAttribute(state, self.key, options, path) - def create_row_processor(self, selectcontext, mapper, row): + def create_row_processor(self, selectcontext, path, mapper, row, adapter): if not self.is_class_level or len(selectcontext.options): - def new_execute(instance, row, ispostselect, **flags): - if not ispostselect: - 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 class-level behavior - - self._init_instance_attribute(instance, callable_=self.setup_loader(instance, selectcontext.options, selectcontext.query._current_path + selectcontext.path)) - return (new_execute, None, None) + path = path + (self.key,) + def new_execute(state, row, **flags): + # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader, + # which will override the class-level behavior + self._init_instance_attribute(state, callable_=self.setup_loader(state, selectcontext.options, selectcontext.query._current_path + path)) + + if self._should_log_debug: + new_execute = self.debug_callable(new_execute, self.logger, None, + lambda state, row, **flags: "set instance-level lazy loader on %s" % mapperutil.state_attribute_str(state, self.key) + ) + + return (new_execute, None) else: - def new_execute(instance, row, ispostselect, **flags): - if not ispostselect: - 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. - instance._state.reset(self.key) - return (new_execute, None, None) + def new_execute(state, row, **flags): + # 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. + state.reset(self.key) + + if self._should_log_debug: + new_execute = self.debug_callable(new_execute, self.logger, None, + lambda state, row, **flags: "set class-level lazy loader on %s" % mapperutil.state_attribute_str(state, self.key) + ) + + return (new_execute, None) def __create_lazy_clause(cls, prop, reverse_direction=False): binds = {} @@ -374,16 +404,16 @@ class LazyLoader(AbstractRelationLoader): binds[col] = sql.bindparam(None, None, type_=col.type) return binds[col] return None - - lazywhere = prop.primaryjoin + lazywhere = prop.primaryjoin + if not prop.secondaryjoin or not reverse_direction: - lazywhere = visitors.traverse(lazywhere, before_clone=col_to_bind, clone=True) + lazywhere = visitors.replacement_traverse(lazywhere, {}, col_to_bind) if prop.secondaryjoin is not None: secondaryjoin = prop.secondaryjoin if reverse_direction: - secondaryjoin = visitors.traverse(secondaryjoin, before_clone=col_to_bind, clone=True) + secondaryjoin = visitors.replacement_traverse(secondaryjoin, {}, col_to_bind) lazywhere = sql.and_(lazywhere, secondaryjoin) bind_to_col = dict([(binds[col].key, col) for col in binds]) @@ -391,47 +421,44 @@ class LazyLoader(AbstractRelationLoader): return (lazywhere, bind_to_col, equated_columns) __create_lazy_clause = classmethod(__create_lazy_clause) -LazyLoader.logger = logging.class_logger(LazyLoader) +LazyLoader.logger = log.class_logger(LazyLoader) class LoadLazyAttribute(object): - """callable, serializable loader object used by LazyLoader""" + """serializable loader object used by LazyLoader""" - def __init__(self, instance, key, options, path): - self.instance = instance + def __init__(self, state, key, options, path): + self.state = state self.key = key self.options = options self.path = path def __getstate__(self): - return {'instance':self.instance, 'key':self.key, 'options':self.options, 'path':serialize_path(self.path)} + return {'state':self.state, 'key':self.key, 'options':self.options, 'path':serialize_path(self.path)} def __setstate__(self, state): - self.instance = state['instance'] + self.state = state['state'] self.key = state['key'] - self.options= state['options'] + self.options = state['options'] self.path = deserialize_path(state['path']) def __call__(self): - instance = self.instance - - if not mapper.has_identity(instance): + state = self.state + if not mapper._state_has_identity(state): return None - instance_mapper = mapper.object_mapper(instance) + instance_mapper = mapper._state_mapper(state) prop = instance_mapper.get_property(self.key) strategy = prop._get_strategy(LazyLoader) if strategy._should_log_debug: - strategy.logger.debug("lazy load attribute %s on instance %s" % (self.key, mapperutil.instance_str(instance))) + strategy.logger.debug("loading %s" % mapperutil.state_attribute_str(state, self.key)) - session = sessionlib.object_session(instance) + session = sessionlib._state_session(state) if session is None: - try: - session = instance_mapper.get_session() - except exceptions.InvalidRequestError: - raise exceptions.UnboundExecutionError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (instance.__class__, self.key)) + raise sa_exc.UnboundExecutionError("Parent instance %s is not bound to a Session; lazy load operation of attribute '%s' cannot proceed" % (mapperutil.state_str(state), self.key)) - q = session.query(prop.mapper).autoflush(False) + q = session.query(prop.mapper).autoflush(False)._adapt_all_clauses() + if self.path: q = q._with_current_path(self.path) @@ -441,7 +468,7 @@ class LoadLazyAttribute(object): ident = [] allnulls = True for primary_key in prop.mapper.primary_key: - val = instance_mapper._get_committed_attr_by_column(instance, strategy._equated_columns[primary_key]) + val = instance_mapper._get_committed_state_attr_by_column(state, strategy._equated_columns[primary_key]) allnulls = allnulls and val is None ident.append(val) if allnulls: @@ -450,14 +477,14 @@ class LoadLazyAttribute(object): q = q._conditional_options(*self.options) return q.get(ident) - if strategy.order_by is not False: - q = q.order_by(strategy.order_by) - elif strategy.secondary is not None and strategy.secondary.default_order_by() is not None: - q = q.order_by(strategy.secondary.default_order_by()) + if prop.order_by is not False: + q = q.order_by(prop.order_by) + elif prop.secondary is not None and prop.secondary.default_order_by() is not None: + q = q.order_by(prop.secondary.default_order_by()) if self.options: q = q._conditional_options(*self.options) - q = q.filter(strategy.lazy_clause(instance)) + q = q.filter(strategy.lazy_clause(state)) result = q.all() if strategy.uselist: @@ -478,19 +505,35 @@ class EagerLoader(AbstractRelationLoader): self.join_depth = self.parent_property.join_depth def init_class_attribute(self): - # class-level eager strategy; add the PropertyLoader - # to the parent's list of "eager loaders"; this tells the Query - # that eager loaders will be used in a normal query - self.parent._eager_loaders.add(self.parent_property) - - # initialize a lazy loader on the class level attribute self.parent_property._get_strategy(LazyLoader).init_class_attribute() - def setup_query(self, context, parentclauses=None, parentmapper=None, **kwargs): + def setup_query(self, context, entity, path, adapter, column_collection=None, parentmapper=None, **kwargs): """Add a left outer join to the statement thats being constructed.""" + + path = path + (self.key,) + + # check for user-defined eager alias + if ("eager_row_processor", path) in context.attributes: + clauses = context.attributes[("eager_row_processor", path)] + + adapter = entity._get_entity_clauses(context.query, context) + if adapter and clauses: + context.attributes[("eager_row_processor", path)] = clauses = adapter.wrap(clauses) + elif adapter: + context.attributes[("eager_row_processor", path)] = clauses = adapter + + else: - path = context.path - + clauses = self.__create_eager_join(context, entity, path, adapter, parentmapper) + if not clauses: + return + + context.attributes[("eager_row_processor", path)] = clauses + + for value in self.mapper._iterate_polymorphic_properties(): + value.setup(context, entity, path + (self.mapper.base_mapper,), clauses, parentmapper=self.mapper, column_collection=context.secondary_columns) + + def __create_eager_join(self, context, entity, path, adapter, parentmapper): # check for join_depth or basic recursion, # if the current path was not explicitly stated as # a desired "loaderstrategy" (i.e. via query.options()) @@ -502,159 +545,148 @@ class EagerLoader(AbstractRelationLoader): if self.mapper.base_mapper in path: return - if ("eager_row_processor", path) in context.attributes: - # if user defined eager_row_processor, that's contains_eager(). - # don't render LEFT OUTER JOIN, generate an AliasedClauses from - # the decorator (this is a hack here, cleaned up in 0.5) - cl = context.attributes[("eager_row_processor", path)] - if cl: - row = cl(None) - class ActsLikeAliasedClauses(object): - def aliased_column(self, col): - return row.map[col] - clauses = ActsLikeAliasedClauses() - else: - clauses = None - else: - clauses = self.__create_eager_join(context, path, parentclauses, parentmapper, **kwargs) - if not clauses: - return - - for value in self.mapper._iterate_polymorphic_properties(): - context.exec_with_path(self.mapper, value.key, value.setup, context, parentclauses=clauses, parentmapper=self.mapper) - - def __create_eager_join(self, context, path, parentclauses, parentmapper, **kwargs): if parentmapper is None: - localparent = context.mapper + localparent = entity.mapper else: localparent = parentmapper - - if context.eager_joins: - towrap = context.eager_joins + + # whether or not the Query will wrap the selectable in a subquery, + # and then attach eager load joins to that (i.e., in the case of LIMIT/OFFSET etc.) + should_nest_selectable = context.query._should_nest_selectable + + if entity in context.eager_joins: + entity_key, default_towrap = entity, entity.selectable + elif should_nest_selectable or not context.from_clause or not sql_util.search(context.from_clause, entity.selectable): + # if no from_clause, or a from_clause we can't join to, or a subquery is going to be generated, + # store eager joins per _MappedEntity; Query._compile_context will + # add them as separate selectables to the select(), or splice them together + # after the subquery is generated + entity_key, default_towrap = entity, entity.selectable else: - towrap = context.from_clause - - # create AliasedClauses object to build up the eager query. this is cached after 1st creation. + # otherwise, create a single eager join from the from clause. + # Query._compile_context will adapt as needed and append to the + # FROM clause of the select(). + entity_key, default_towrap = None, context.from_clause + + towrap = context.eager_joins.setdefault(entity_key, default_towrap) + + # create AliasedClauses object to build up the eager query. this is cached after 1st creation. + # this also allows ORMJoin to cache the aliased joins it produces since we pass the same + # args each time in the typical case. + path_key = util.WeakCompositeKey(*path) try: - clauses = self.clauses[path] + clauses = self.clauses[path_key] except KeyError: - clauses = mapperutil.PropertyAliasedClauses(self.parent_property, self.parent_property.primaryjoin, self.parent_property.secondaryjoin, parentclauses) - self.clauses[path] = clauses + self.clauses[path_key] = clauses = mapperutil.ORMAdapter(mapperutil.AliasedClass(self.mapper), + equivalents=self.mapper._equivalent_columns, + chain_to=adapter) - # place the "row_decorator" from the AliasedClauses into the QueryContext, where it will - # be picked up in create_row_processor() when results are fetched - context.attributes[("eager_row_processor", path)] = clauses.row_decorator - - if self.secondaryjoin is not None: - context.eager_joins = sql.outerjoin(towrap, clauses.secondary, clauses.primaryjoin).outerjoin(clauses.alias, clauses.secondaryjoin) - - # TODO: check for "deferred" cols on parent/child tables here ? this would only be - # useful if the primary/secondaryjoin are against non-PK columns on the tables (and therefore might be deferred) - - if self.order_by is False and self.secondary.default_order_by() is not None: - context.eager_order_by += clauses.secondary.default_order_by() + if adapter: + if getattr(adapter, 'aliased_class', None): + onclause = getattr(adapter.aliased_class, self.key, self.parent_property) + else: + onclause = getattr(mapperutil.AliasedClass(self.parent, adapter.selectable), self.key, self.parent_property) else: - context.eager_joins = towrap.outerjoin(clauses.alias, clauses.primaryjoin) - # ensure all the cols on the parent side are actually in the + onclause = self.parent_property + + context.eager_joins[entity_key] = eagerjoin = mapperutil.outerjoin(towrap, clauses.aliased_class, onclause) + + # send a hint to the Query as to where it may "splice" this join + eagerjoin.stop_on = entity.selectable + + if not self.parent_property.secondary and context.query._should_nest_selectable and not parentmapper: + # for parentclause that is the non-eager end of the join, + # ensure all the parent cols in the primaryjoin are actually in the # columns clause (i.e. are not deferred), so that aliasing applied by the Query propagates # those columns outward. This has the effect of "undefering" those columns. - for col in sql_util.find_columns(clauses.primaryjoin): + for col in sql_util.find_columns(self.parent_property.primaryjoin): if localparent.mapped_table.c.contains_column(col): + if adapter: + col = adapter.columns[col] context.primary_columns.append(col) - - if self.order_by is False and clauses.alias.default_order_by() is not None: - context.eager_order_by += clauses.alias.default_order_by() - - if clauses.order_by: - context.eager_order_by += util.to_list(clauses.order_by) + if self.parent_property.order_by is False: + if self.parent_property.secondaryjoin: + default_order_by = eagerjoin.left.right.default_order_by() + else: + default_order_by = eagerjoin.right.default_order_by() + if default_order_by: + context.eager_order_by += default_order_by + elif self.parent_property.order_by: + context.eager_order_by += eagerjoin._target_adapter.copy_and_process(util.to_list(self.parent_property.order_by)) + return clauses - 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]) - - if ("eager_row_processor", path) in selectcontext.attributes: - decorator = selectcontext.attributes[("eager_row_processor", path)] - if decorator is None: - decorator = lambda row: row + def __create_eager_adapter(self, context, row, adapter, path): + if ("eager_row_processor", path) in context.attributes: + decorator = context.attributes[("eager_row_processor", path)] else: if self._should_log_debug: self.logger.debug("Could not locate aliased clauses for key: " + str(path)) - return None + return False + if adapter and decorator: + decorator = adapter.wrap(decorator) + elif adapter: + decorator = adapter + try: - decorated_row = decorator(row) - # check for identity key - identity_key = self.mapper.identity_key_from_row(decorated_row) - # and its good + identity_key = self.mapper.identity_key_from_row(row, decorator) return decorator except KeyError, k: # no identity key - dont return a row processor, will cause a degrade to lazy if self._should_log_debug: - self.logger.debug("could not locate identity key from row '%s'; missing column '%s'" % (repr(decorated_row), str(k))) - return None - - def create_row_processor(self, selectcontext, mapper, row): + self.logger.debug("could not locate identity key from row; missing column '%s'" % k) + return False - row_decorator = self._create_row_decorator(selectcontext, row, selectcontext.path) - pathstr = ','.join([str(x) for x in selectcontext.path]) - if row_decorator is not None: - def execute(instance, row, isnew, **flags): - decorated_row = row_decorator(row) - - if not self.uselist: - if self._should_log_debug: - self.logger.debug("eagerload scalar instance on %s" % mapperutil.attribute_str(instance, self.key)) + def create_row_processor(self, context, path, mapper, row, adapter): + path = path + (self.key,) + eager_adapter = self.__create_eager_adapter(context, row, adapter, path) + + if eager_adapter is not False: + key = self.key + _instance = self.mapper._instance_processor(context, path + (self.mapper.base_mapper,), eager_adapter) + + if not self.uselist: + def execute(state, row, isnew, **flags): if isnew: # set a scalar object instance directly on the # parent object, bypassing InstrumentedAttribute # event handlers. - # - instance.__dict__[self.key] = self.mapper._instance(selectcontext, decorated_row, None) + state.dict[key] = _instance(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 or self.key not in instance._state.appenders: - # appender_key can be absent from selectcontext.attributes with isnew=False + _instance(row, None) + else: + def execute(state, row, isnew, **flags): + if isnew or (state, key) not in context.attributes: + # appender_key can be absent from context.attributes with isnew=False # when self-referential eager loading is used; the same instance may be present # in two distinct sets of result columns - - if self._should_log_debug: - self.logger.debug("initialize UniqueAppender on %s" % mapperutil.attribute_str(instance, self.key)) - collection = attributes.init_collection(instance, self.key) + collection = attributes.init_collection(state, key) appender = util.UniqueAppender(collection, 'append_without_event') - instance._state.appenders[self.key] = appender - - result_list = instance._state.appenders[self.key] - if self._should_log_debug: - self.logger.debug("eagerload list instance on %s" % mapperutil.attribute_str(instance, self.key)) + context.attributes[(state, key)] = appender + + result_list = context.attributes[(state, key)] - self.mapper._instance(selectcontext, decorated_row, result_list) + _instance(row, result_list) if self._should_log_debug: - self.logger.debug("Returning eager instance loader for %s" % str(self)) + execute = self.debug_callable(execute, self.logger, + "%s returning eager instance loader" % self, + lambda state, row, isnew, **flags: "%s eagerload %s" % (self, self.uselist and "scalar attribute" or "collection") + ) - return (execute, execute, None) + return (execute, execute) else: if self._should_log_debug: - self.logger.debug("eager loader %s degrading to lazy loader" % str(self)) - return self.parent_property._get_strategy(LazyLoader).create_row_processor(selectcontext, mapper, row) + self.logger.debug("%s degrading to lazy loader" % self) + return self.parent_property._get_strategy(LazyLoader).create_row_processor(context, path, mapper, row, adapter) - def __str__(self): - return str(self.parent) + "." + self.key - -EagerLoader.logger = logging.class_logger(EagerLoader) +EagerLoader.logger = log.class_logger(EagerLoader) class EagerLazyOption(StrategizedOption): def __init__(self, key, lazy=True, chained=False, mapper=None): @@ -665,20 +697,6 @@ class EagerLazyOption(StrategizedOption): def is_chained(self): return not self.lazy and self.chained - def process_query_property(self, query, paths): - if self.lazy: - if paths[-1] in query._eager_loaders: - query._eager_loaders = query._eager_loaders.difference(util.Set([paths[-1]])) - else: - if not self.chained: - paths = [paths[-1]] - res = util.Set() - for path in paths: - if len(path) - len(query._current_path) == 2: - res.add(path) - query._eager_loaders = query._eager_loaders.union(res) - super(EagerLazyOption, self).process_query_property(query, paths) - def get_strategy_class(self): if self.lazy: return LazyLoader @@ -687,24 +705,26 @@ class EagerLazyOption(StrategizedOption): elif self.lazy is None: return NoLoader -EagerLazyOption.logger = logging.class_logger(EagerLazyOption) - -class RowDecorateOption(PropertyOption): - def __init__(self, key, decorator=None, alias=None): - super(RowDecorateOption, self).__init__(key) - self.decorator = decorator +class LoadEagerFromAliasOption(PropertyOption): + def __init__(self, key, alias=None): + super(LoadEagerFromAliasOption, self).__init__(key) + if alias: + if not isinstance(alias, basestring): + m, alias, is_aliased_class = mapperutil._entity_info(alias) self.alias = alias def process_query_property(self, query, paths): - if self.alias is not None and self.decorator is None: - (mapper, propname) = paths[-1][-2:] - - prop = mapper.get_property(propname, resolve_synonyms=True) + if self.alias: if isinstance(self.alias, basestring): - self.alias = prop.target.alias(self.alias) + (mapper, propname) = paths[-1][-2:] - self.decorator = mapperutil.create_row_adapter(self.alias) - query._attributes[("eager_row_processor", paths[-1])] = self.decorator + prop = mapper.get_property(propname, resolve_synonyms=True) + self.alias = prop.target.alias(self.alias) + if not isinstance(self.alias, expression.Alias): + import pdb + pdb.set_trace() + query._attributes[("eager_row_processor", paths[-1])] = sql_util.ColumnAdapter(self.alias) + else: + query._attributes[("eager_row_processor", paths[-1])] = None -RowDecorateOption.logger = logging.class_logger(RowDecorateOption) |
