Commits

Mike Bayer committed 0095a03

- 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]

Comments (0)

Files changed (5)

       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]

lib/sqlalchemy/orm/attributes.py

     key = None
     runid = None
     expired_attributes = EMPTY_SET
+    load_options = EMPTY_SET
+    load_path = ()
     insert_order = None
     
     def __init__(self, obj, manager):
                 '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}
         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__

lib/sqlalchemy/orm/mapper.py

         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)
 

lib/sqlalchemy/orm/query.py

         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):

lib/sqlalchemy/orm/strategies.py

         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:
              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
         )
 
             
             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 
         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]
         _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,
                 )
             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,
             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:
                 )
 
             return (new_execute, None)
-
+            
     def _create_lazy_clause(cls, prop, reverse_direction=False):
         binds = util.column_dict()
         lookup = util.column_dict()
 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)
         
         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
                 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()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.