From: Mike Bayer Date: Sat, 2 May 2009 17:41:04 +0000 (+0000) Subject: - MapperOptions and other state associated with query.options() X-Git-Tag: rel_0_5_4~12 X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=f8daff6da185188d8ed807a7f250320e291ad523;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - MapperOptions and other state associated with query.options() is no longer bundled within callables associated with each lazy/deferred-loading attribute during a load. The options are now associated with the instance's state object just once when it's populated. This removes the need in most cases for per-instance/attribute loader objects, improving load speed and memory overhead for individual instances. [ticket:1391] --- diff --git a/CHANGES b/CHANGES index c3dc89916f..e950ca0a1f 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,15 @@ CHANGES parameters but this would require further development. [ticket:1357] + - MapperOptions and other state associated with query.options() + is no longer bundled within callables associated with each + lazy/deferred-loading attribute during a load. + The options are now associated with the instance's + state object just once when it's populated. This removes + the need in most cases for per-instance/attribute loader + objects, improving load speed and memory overhead for + individual instances. [ticket:1391] + - Fixed another location where autoflush was interfering with session.merge(). autoflush is disabled completely for the duration of merge() now. [ticket:1360] diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index b39ef990eb..68aa0d93ae 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -828,6 +828,8 @@ class InstanceState(object): key = None runid = None expired_attributes = EMPTY_SET + load_options = EMPTY_SET + load_path = () insert_order = None def __init__(self, obj, manager): @@ -937,6 +939,8 @@ class InstanceState(object): 'parents': self.parents, 'modified': self.modified, 'expired':self.expired, + 'load_options':self.load_options, + 'load_path':interfaces.serialize_path(self.load_path), 'instance': self.obj(), 'expired_attributes':self.expired_attributes, 'callables': self.callables} @@ -949,6 +953,8 @@ class InstanceState(object): self.pending = state['pending'] self.modified = state['modified'] self.obj = weakref.ref(state['instance']) + self.load_options = state['load_options'] or EMPTY_SET + self.load_path = interfaces.deserialize_path(state['load_path']) self.class_ = self.obj().__class__ self.manager = manager_of_class(self.class_) self.dict = self.obj().__dict__ diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 0805407268..e5dbb4d039 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1515,6 +1515,12 @@ class Mapper(object): existing_populators = [] def populate_state(state, row, isnew, only_load_props, **flags): + if isnew: + if context.options: + state.load_options = context.options + if state.load_options: + state.load_path = context.query._current_path + path + if not new_populators: new_populators[:], existing_populators[:] = self._populators(context, path, row, adapter) diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index e8169faa2e..28ddcc5eaa 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -2115,7 +2115,7 @@ class QueryContext(object): self.froms = [] self.adapter = None - self.options = query._with_options + self.options = set(query._with_options) self.attributes = query._attributes.copy() class AliasOption(interfaces.MapperOption): diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index dc64b283d6..1aeb311e1c 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -201,11 +201,13 @@ class DeferredColumnLoader(LoaderStrategy): 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): + elif not self.is_class_level: def new_execute(state, row, **flags): - state.set_callable(self.key, self.setup_loader(state)) + state.set_callable(self.key, LoadDeferredColumns(state, self.key)) else: def new_execute(state, row, **flags): + # reset state on the key so that deferred callables + # fire off on next access. state.reset(self.key) if self._should_log_debug: @@ -227,7 +229,7 @@ class DeferredColumnLoader(LoaderStrategy): compare_function=self.columns[0].type.compare_values, copy_function=self.columns[0].type.copy_value, mutable_scalars=self.columns[0].type.is_mutable(), - callable_=self.class_level_loader, + callable_=self._class_level_loader, dont_expire_missing=True ) @@ -238,46 +240,31 @@ class DeferredColumnLoader(LoaderStrategy): self.parent_property._get_strategy(ColumnLoader).setup_query(context, entity, path, adapter, **kwargs) - def class_level_loader(self, state, props=None): + def _class_level_loader(self, state): if not mapperutil._state_has_identity(state): return None - localparent = mapper._state_mapper(state) - - # adjust for the ColumnProperty associated with the instance - # not being our own ColumnProperty. - # TODO: this may no longer be relevant without entity_name. - prop = localparent.get_property(self.key) - if prop is not self.parent_property: - return prop._get_strategy(DeferredColumnLoader).setup_loader(state) - - return LoadDeferredColumns(state, self.key, props) + return LoadDeferredColumns(state, self.key) - def setup_loader(self, state, props=None, create_statement=None): - return LoadDeferredColumns(state, self.key, props) log.class_logger(DeferredColumnLoader) class LoadDeferredColumns(object): """serializable loader object used by DeferredColumnLoader""" - def __init__(self, *args): - self.state, self.key, self.keys = args + def __init__(self, state, key): + self.state, self.key = state, key def __call__(self): state = self.state - if not mapper._state_has_identity(state): - return None localparent = mapper._state_mapper(state) prop = localparent.get_property(self.key) strategy = prop._get_strategy(DeferredColumnLoader) - if self.keys: - toload = self.keys - elif strategy.group: + if strategy.group: toload = [ p.key for p in localparent.iterate_properties @@ -292,11 +279,18 @@ class LoadDeferredColumns(object): 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.state_attribute_str(state, 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._state_session(state) if session is None: - 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)) + 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) ident = state.key[1] @@ -380,7 +374,7 @@ class LazyLoader(AbstractRelationLoader): _register_attribute(self, mapper, useobject=True, - callable_=self.class_level_loader, + callable_=self._class_level_loader, uselist = self.parent_property.uselist, typecallable = self.parent_property.collection_class, ) @@ -435,31 +429,20 @@ class LazyLoader(AbstractRelationLoader): criterion = adapt_source(criterion) return criterion - def class_level_loader(self, state, options=None, path=None): + def _class_level_loader(self, state): if not mapperutil._state_has_identity(state): return None - localparent = mapper._state_mapper(state) - - # adjust for the PropertyLoader associated with the instance - # not being our own PropertyLoader. - # TODO: this may no longer be relevant without entity_name - prop = localparent.get_property(self.key) - if prop is not self.parent_property: - return prop._get_strategy(LazyLoader).setup_loader(state) - - return LoadLazyAttribute(state, self.key, options, path) - - def setup_loader(self, state, options=None, path=None): - return LoadLazyAttribute(state, self.key, options, path) + return LoadLazyAttribute(state, self.key) def create_row_processor(self, selectcontext, path, mapper, row, adapter): - if not self.is_class_level or len(selectcontext.options): - path = path + (self.key,) + if not self.is_class_level: 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)) + # which will override the class-level behavior. + # this currently only happens when using a "lazyload" option on a "no load" attribute - + # "eager" attributes always have a class-level lazyloader installed. + self._init_instance_attribute(state, callable_=LoadLazyAttribute(state, self.key)) if self._should_log_debug: new_execute = self.debug_callable(new_execute, self.logger, None, @@ -471,8 +454,7 @@ class LazyLoader(AbstractRelationLoader): 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. + # this is needed in populate_existing() types of scenarios to reset any existing state. state.reset(self.key) if self._should_log_debug: @@ -481,7 +463,7 @@ class LazyLoader(AbstractRelationLoader): ) return (new_execute, None) - + def _create_lazy_clause(cls, prop, reverse_direction=False): binds = util.column_dict() lookup = util.column_dict() @@ -529,20 +511,17 @@ log.class_logger(LazyLoader) class LoadLazyAttribute(object): """serializable loader object used by LazyLoader""" - def __init__(self, *args): - self.state, self.key, self.options, self.path = args + def __init__(self, state, key): + self.state, self.key = state, key def __getstate__(self): - return (self.state, self.key, self.options, serialize_path(self.path)) + return (self.state, self.key) def __setstate__(self, state): - self.state, self.key, self.options, path = state - self.path = deserialize_path(path) + self.state, self.key = state def __call__(self): state = self.state - if not mapper._state_has_identity(state): - return None instance_mapper = mapper._state_mapper(state) prop = instance_mapper.get_property(self.key) @@ -561,8 +540,8 @@ class LoadLazyAttribute(object): q = session.query(prop.mapper)._adapt_all_clauses() - if self.path: - q = q._with_current_path(self.path) + if state.load_path: + q = q._with_current_path(state.load_path + (self.key,)) # if we have a simple primary key load, use mapper.get() # to possibly save a DB round trip @@ -575,15 +554,15 @@ class LoadLazyAttribute(object): ident.append(val) if allnulls: return None - if self.options: - q = q._conditional_options(*self.options) + if state.load_options: + q = q._conditional_options(*state.load_options) return q.get(ident) if prop.order_by: q = q.order_by(*util.to_list(prop.order_by)) - if self.options: - q = q._conditional_options(*self.options) + if state.load_options: + q = q._conditional_options(*state.load_options) q = q.filter(strategy.lazy_clause(state)) result = q.all()