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