From: Mike Bayer Date: Fri, 15 Oct 2010 15:59:02 +0000 (-0400) Subject: - Added a new "lazyload" option "immediateload". X-Git-Tag: rel_0_6_5~21 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=295fd901258751f42192dd506a2fcd4af7b46d58;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Added a new "lazyload" option "immediateload". Issues the usual "lazy" load operation automatically as the object is populated. The use case here is when loading objects to be placed in an offline cache, or otherwise used after the session isn't available, and straight 'select' loading, not 'joined' or 'subquery', is desired. [ticket:1914] --- diff --git a/CHANGES b/CHANGES index 1d78847145..2cf9a92546 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,15 @@ CHANGES 0.6.5 ===== - orm + - Added a new "lazyload" option "immediateload". + Issues the usual "lazy" load operation automatically + as the object is populated. The use case + here is when loading objects to be placed in + an offline cache, or otherwise used after + the session isn't available, and straight 'select' + loading, not 'joined' or 'subquery', is desired. + [ticket:1914] + - Fixed recursion bug which could occur when moving an object from one reference to another, with backrefs involved, where the initiating parent diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 8b32d1a273..d4e436b3b1 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -84,6 +84,7 @@ __all__ = ( 'eagerload', 'eagerload_all', 'extension', + 'immediateload', 'join', 'joinedload', 'joinedload_all', @@ -335,7 +336,12 @@ def relationship(argument, secondary=None, **kwargs): ``select``. Values include: * ``select`` - items should be loaded lazily when the property is first - accessed, using a separate SELECT statement. + accessed, using a separate SELECT statement, or identity map + fetch for simple many-to-one references. + + * ``immediate`` - items should be loaded as the parents are loaded, + using a separate SELECT statement, or identity map fetch for + simple many-to-one references. (new as of 0.6.5) * ``joined`` - items should be loaded "eagerly" in the same query as that of the parent, using a JOIN or LEFT OUTER JOIN. Whether @@ -1122,7 +1128,7 @@ def subqueryload_all(*keys): query.options(subqueryload_all(User.orders, Order.items, Item.keywords)) - See also: :func:`joinedload_all`, :func:`lazyload` + See also: :func:`joinedload_all`, :func:`lazyload`, :func:`immediateload` """ return strategies.EagerLazyOption(keys, lazy="subquery", chained=True) @@ -1134,7 +1140,7 @@ def lazyload(*keys): Used with :meth:`~sqlalchemy.orm.query.Query.options`. - See also: :func:`eagerload`, :func:`subqueryload` + See also: :func:`eagerload`, :func:`subqueryload`, :func:`immediateload` """ return strategies.EagerLazyOption(keys, lazy=True) @@ -1145,11 +1151,24 @@ def noload(*keys): Used with :meth:`~sqlalchemy.orm.query.Query.options`. - See also: :func:`lazyload`, :func:`eagerload`, :func:`subqueryload` + See also: :func:`lazyload`, :func:`eagerload`, :func:`subqueryload`, :func:`immediateload` """ return strategies.EagerLazyOption(keys, lazy=None) +def immediateload(*keys): + """Return a ``MapperOption`` that will convert the property of the given + name into an immediate load. + + Used with :meth:`~sqlalchemy.orm.query.Query.options`. + + See also: :func:`lazyload`, :func:`eagerload`, :func:`subqueryload` + + New as of verison 0.6.5. + + """ + return strategies.EagerLazyOption(keys, lazy='immediate') + def contains_alias(alias): """Return a ``MapperOption`` that will indicate to the query that the main table has been aliased. diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index c5ddaca40b..dd2c6e8960 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -36,7 +36,7 @@ class DynaLoader(strategies.AbstractRelationshipLoader): ) def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return (None, None) + return None, None, None log.class_logger(DynaLoader) diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index fa27859ecb..c3c9c754fb 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -436,38 +436,8 @@ class MapperProperty(object): pass def create_row_processor(self, selectcontext, path, mapper, row, adapter): - """Return a 2-tuple consiting of two row processing functions and - an instance post-processing function. - - Input arguments are the query.SelectionContext and the *first* - applicable row of a result set obtained within - query.Query.instances(), called only the first time a particular - mapper's populate_instance() method is invoked for the overall result. - - The settings contained within the SelectionContext as well as the - columns present in the row (which will be the same columns present in - all rows) are used to determine the presence and behavior of the - returned callables. The callables will then be used to process all - rows and instances. - - Callables are of the following form:: - - def new_execute(state, dict_, row, isnew): - # process incoming instance state and given row. - # the instance is - # "new" and was just created upon receipt of this row. - "isnew" indicates if the instance was newly created as a - result of reading this row - - def existing_execute(state, dict_, row): - # process incoming instance state and given row. the - # instance is - # "existing" and was created based on a previous row. - - return (new_execute, existing_execute) - - Either of the three tuples can be ``None`` in which case no function - is called. + """Return a 3-tuple consisting of three row processing functions. + """ raise NotImplementedError() diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 378570723b..a14ad647a8 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2135,10 +2135,11 @@ class Mapper(object): state.load_path = load_path if not new_populators: - new_populators[:], existing_populators[:] = \ - self._populators(context, path, row, - adapter) - + self._populators(context, path, row, adapter, + new_populators, + existing_populators + ) + if isnew: populators = new_populators else: @@ -2309,20 +2310,24 @@ class Mapper(object): return instance return _instance - def _populators(self, context, path, row, adapter): + def _populators(self, context, path, row, adapter, + new_populators, existing_populators): """Produce a collection of attribute level row processor callables.""" - new_populators, existing_populators = [], [] + delayed_populators = [] for prop in self._props.itervalues(): - newpop, existingpop = prop.create_row_processor( + newpop, existingpop, delayedpop = prop.create_row_processor( context, path, self, row, adapter) if newpop: new_populators.append((prop.key, newpop)) if existingpop: existing_populators.append((prop.key, existingpop)) - return new_populators, existing_populators - + if delayedpop: + delayed_populators.append((prop.key, delayedpop)) + if delayed_populators: + new_populators.extend(delayed_populators) + def _configure_subclass_mapper(self, context, path, adapter): """Produce a mapper level row processor callable factory for mappers inheriting this one.""" diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 4efd2acc90..0cbbf630d4 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -255,7 +255,7 @@ class DescriptorProperty(MapperProperty): pass def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return (None, None) + return None, None, None def merge(self, session, source_state, source_dict, dest_state, dest_dict, load, _recursive): diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 3e6b6a21f6..60454eabc2 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -88,7 +88,7 @@ class UninstrumentedColumnLoader(LoaderStrategy): column_collection.append(c) def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return None, None + return None, None, None class ColumnLoader(LoaderStrategy): """Strategize the loading of a plain column-based MapperProperty.""" @@ -127,11 +127,11 @@ class ColumnLoader(LoaderStrategy): if col is not None and col in row: def new_execute(state, dict_, row): dict_[key] = row[col] - return new_execute, None + return new_execute, None, None else: def new_execute(state, dict_, row): state.expire_attribute_pre_commit(dict_, key) - return new_execute, None + return new_execute, None, None log.class_logger(ColumnLoader) @@ -184,7 +184,7 @@ class CompositeColumnLoader(ColumnLoader): def new_execute(state, dict_, row): dict_[key] = composite_class(*[row[c] for c in columns]) - return new_execute, None + return new_execute, None, None log.class_logger(CompositeColumnLoader) @@ -211,7 +211,7 @@ class DeferredColumnLoader(LoaderStrategy): # fire off on next access. state.reset(dict_, key) - return new_execute, None + return new_execute, None, None def init(self): if hasattr(self.parent_property, 'composite_class'): @@ -348,7 +348,7 @@ class NoLoader(AbstractRelationshipLoader): def create_row_processor(self, selectcontext, path, mapper, row, adapter): def new_execute(state, dict_, row): state.initialize(self.key) - return new_execute, None + return new_execute, None, None log.class_logger(NoLoader) @@ -509,7 +509,7 @@ class LazyLoader(AbstractRelationshipLoader): # any existing state. state.reset(dict_, key) - return new_execute, None + return new_execute, None, None @classmethod def _create_lazy_clause(cls, prop, reverse_direction=False): @@ -683,6 +683,23 @@ class LoadLazyAttribute(object): else: return None +class ImmediateLoader(AbstractRelationshipLoader): + def init_class_attribute(self, mapper): + self.parent_property.\ + _get_strategy(LazyLoader).\ + init_class_attribute(mapper) + + def setup_query(self, context, entity, + path, adapter, column_collection=None, + parentmapper=None, **kwargs): + pass + + def create_row_processor(self, context, path, mapper, row, adapter): + def execute(state, dict_, row): + state.get_impl(self.key).get(state, dict_) + + return None, None, execute + class SubqueryLoader(AbstractRelationshipLoader): def init(self): super(SubqueryLoader, self).init() @@ -859,7 +876,7 @@ class SubqueryLoader(AbstractRelationshipLoader): path = interfaces._reduce_path(path) if ('subquery', path) not in context.attributes: - return None, None + return None, None, None local_cols, remote_cols = self._local_remote_columns(self.parent_property) @@ -903,7 +920,7 @@ class SubqueryLoader(AbstractRelationshipLoader): state.get_impl(self.key).\ set_committed_value(state, dict_, scalar) - return execute, None + return execute, None, None log.class_logger(SubqueryLoader) @@ -1156,7 +1173,7 @@ class EagerLoader(AbstractRelationshipLoader): "Multiple rows returned with " "uselist=False for eagerly-loaded attribute '%s' " % self) - return new_execute, existing_execute + return new_execute, existing_execute, None else: def new_execute(state, dict_, row): collection = attributes.init_state_collection( @@ -1181,7 +1198,7 @@ class EagerLoader(AbstractRelationshipLoader): 'append_without_event') context.attributes[(state, key)] = result_list _instance(row, result_list) - return new_execute, existing_execute + return new_execute, existing_execute, None else: return self.parent_property.\ _get_strategy(LazyLoader).\ @@ -1221,6 +1238,8 @@ def factory(identifier): return LazyLoader elif identifier == 'subquery': return SubqueryLoader + elif identifier == 'immediate': + return ImmediateLoader else: return LazyLoader