From: Mike Bayer Date: Wed, 22 Oct 2008 16:09:19 +0000 (+0000) Subject: - Added more granularity to internal attribute access, such X-Git-Tag: rel_0_5rc3~56 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c35621969027ae052bdcff98a6c7d30e98e54a0e;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Added more granularity to internal attribute access, such that cascade and flush operations will not initialize unloaded attributes and collections, leaving them intact for a lazy-load later on. Backref events still initialize attrbutes and collections for pending instances. [ticket:1202] --- diff --git a/CHANGES b/CHANGES index fd8a71b91b..f934b716d1 100644 --- a/CHANGES +++ b/CHANGES @@ -24,6 +24,13 @@ CHANGES - polymorphic_union() function respects the "key" of each Column if they differ from the column's name. + + - Added more granularity to internal attribute access, such + that cascade and flush operations will not initialize + unloaded attributes and collections, leaving them intact for + a lazy-load later on. Backref events still initialize + attrbutes and collections for pending instances. + [ticket:1202] - sql - Further simplified SELECT compilation and its relationship diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 0424d9b7c6..4429d44f41 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -34,6 +34,21 @@ ATTR_WAS_SET = util.symbol('ATTR_WAS_SET') NO_VALUE = util.symbol('NO_VALUE') NEVER_SET = util.symbol('NEVER_SET') +# "passive" get settings +# TODO: the True/False values need to be factored out +# of the rest of ORM code +# don't fire off any callables, and don't initialize the attribute to +# an empty value +PASSIVE_NO_INITIALIZE = True #util.symbol('PASSIVE_NO_INITIALIZE') + +# don't fire off any callables, but if no callables present +# then initialize to an empty value/collection +# this is used by backrefs. +PASSIVE_NO_CALLABLES = util.symbol('PASSIVE_NO_CALLABLES') + +# fire callables/initialize as needed +PASSIVE_OFF = False #util.symbol('PASSIVE_OFF') + INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__' """Attribute, elects custom instrumentation when present on a mapped class. @@ -290,7 +305,7 @@ class AttributeImpl(object): else: state.callables[self.key] = callable_ - def get_history(self, state, passive=False): + def get_history(self, state, passive=PASSIVE_OFF): raise NotImplementedError() def _get_callable(self, state): @@ -307,7 +322,7 @@ class AttributeImpl(object): state.dict[self.key] = None return None - def get(self, state, passive=False): + def get(self, state, passive=PASSIVE_OFF): """Retrieve a value from the given object. If a callable is assembled on this object's attribute, and @@ -320,9 +335,12 @@ class AttributeImpl(object): except KeyError: # if no history, check for lazy callables, etc. if state.committed_state.get(self.key, NEVER_SET) is NEVER_SET: + if passive is PASSIVE_NO_INITIALIZE: + return PASSIVE_NORESULT + callable_ = self._get_callable(state) if callable_ is not None: - if passive: + if passive is not PASSIVE_OFF: return PASSIVE_NORESULT value = callable_() if value is not ATTR_WAS_SET: @@ -335,16 +353,16 @@ class AttributeImpl(object): # Return a new, empty value return self.initialize(state) - def append(self, state, value, initiator, passive=False): + def append(self, state, value, initiator, passive=PASSIVE_OFF): self.set(state, value, initiator) - def remove(self, state, value, initiator, passive=False): + def remove(self, state, value, initiator, passive=PASSIVE_OFF): self.set(state, None, initiator) def set(self, state, value, initiator): raise NotImplementedError() - def get_committed_value(self, state, passive=False): + def get_committed_value(self, state, passive=PASSIVE_OFF): """return the unchanged value of this attribute""" if self.key in state.committed_state: @@ -387,7 +405,7 @@ class ScalarAttributeImpl(AttributeImpl): else: del state.dict[self.key] - def get_history(self, state, passive=False): + def get_history(self, state, passive=PASSIVE_OFF): return History.from_attribute( self, state, state.dict.get(self.key, NO_VALUE)) @@ -439,7 +457,7 @@ class MutableScalarAttributeImpl(ScalarAttributeImpl): raise sa_exc.ArgumentError("MutableScalarAttributeImpl requires a copy function") self.copy = copy_function - def get_history(self, state, passive=False): + def get_history(self, state, passive=PASSIVE_OFF): return History.from_attribute( self, state, state.dict.get(self.key, NO_VALUE)) @@ -447,7 +465,7 @@ class MutableScalarAttributeImpl(ScalarAttributeImpl): dest[self.key] = self.copy(state.dict[self.key]) def check_mutable_modified(self, state): - (added, unchanged, deleted) = self.get_history(state, passive=True) + (added, unchanged, deleted) = self.get_history(state, passive=PASSIVE_NO_INITIALIZE) return bool(added or deleted) def set(self, state, value, initiator): @@ -487,7 +505,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): self.fire_remove_event(state, old, self) del state.dict[self.key] - def get_history(self, state, passive=False): + def get_history(self, state, passive=PASSIVE_OFF): if self.key in state.dict: return History.from_attribute(self, state, state.dict[self.key]) else: @@ -569,7 +587,7 @@ class CollectionAttributeImpl(AttributeImpl): def __copy(self, item): return [y for y in list(collections.collection_adapter(item))] - def get_history(self, state, passive=False): + def get_history(self, state, passive=PASSIVE_OFF): current = self.get(state, passive=passive) if current is PASSIVE_NORESULT: return (None, None, None) @@ -577,7 +595,7 @@ class CollectionAttributeImpl(AttributeImpl): return History.from_attribute(self, state, current) def fire_append_event(self, state, value, initiator): - state.modified_event(self, True, NEVER_SET, passive=True) + state.modified_event(self, True, NEVER_SET, passive=PASSIVE_NO_INITIALIZE) if self.trackparent and value is not None: self.sethasparent(instance_state(value), True) @@ -587,10 +605,10 @@ class CollectionAttributeImpl(AttributeImpl): return value def fire_pre_remove_event(self, state, initiator): - state.modified_event(self, True, NEVER_SET, passive=True) + state.modified_event(self, True, NEVER_SET, passive=PASSIVE_NO_INITIALIZE) def fire_remove_event(self, state, value, initiator): - state.modified_event(self, True, NEVER_SET, passive=True) + state.modified_event(self, True, NEVER_SET, passive=PASSIVE_NO_INITIALIZE) if self.trackparent and value is not None: self.sethasparent(instance_state(value), False) @@ -620,7 +638,7 @@ class CollectionAttributeImpl(AttributeImpl): return state.manager.initialize_collection( self.key, state, self.collection_factory) - def append(self, state, value, initiator, passive=False): + def append(self, state, value, initiator, passive=PASSIVE_OFF): if initiator is self: return @@ -631,7 +649,7 @@ class CollectionAttributeImpl(AttributeImpl): else: collection.append_with_event(value, initiator) - def remove(self, state, value, initiator, passive=False): + def remove(self, state, value, initiator, passive=PASSIVE_OFF): if initiator is self: return @@ -721,7 +739,7 @@ class CollectionAttributeImpl(AttributeImpl): return user_data - def get_collection(self, state, user_data=None, passive=False): + def get_collection(self, state, user_data=None, passive=PASSIVE_OFF): """Retrieve the CollectionAdapter associated with the given state. Creates a new CollectionAdapter if one does not exist. @@ -754,23 +772,23 @@ class GenericBackrefExtension(interfaces.AttributeExtension): old_state = instance_state(oldchild) impl = old_state.get_impl(self.key) try: - impl.remove(old_state, state.obj(), initiator, passive=True) + impl.remove(old_state, state.obj(), initiator, passive=PASSIVE_NO_CALLABLES) except (ValueError, KeyError, IndexError): pass if child is not None: new_state = instance_state(child) - new_state.get_impl(self.key).append(new_state, state.obj(), initiator, passive=True) + new_state.get_impl(self.key).append(new_state, state.obj(), initiator, passive=PASSIVE_NO_CALLABLES) return child def append(self, state, child, initiator): child_state = instance_state(child) - child_state.get_impl(self.key).append(child_state, state.obj(), initiator, passive=True) + child_state.get_impl(self.key).append(child_state, state.obj(), initiator, passive=PASSIVE_NO_CALLABLES) return child def remove(self, state, child, initiator): if child is not None: child_state = instance_state(child) - child_state.get_impl(self.key).remove(child_state, state.obj(), initiator, passive=True) + child_state.get_impl(self.key).remove(child_state, state.obj(), initiator, passive=PASSIVE_NO_CALLABLES) class InstanceState(object): @@ -840,12 +858,12 @@ class InstanceState(object): self.pending[key] = PendingCollection() return self.pending[key] - def value_as_iterable(self, key, passive=False): + def value_as_iterable(self, key, passive=PASSIVE_OFF): """return an InstanceState attribute as a list, regardless of it being a scalar or collection-based attribute. - returns None if passive=True and the getter returns + returns None if passive is not PASSIVE_OFF and the getter returns PASSIVE_NORESULT. """ @@ -961,7 +979,7 @@ class InstanceState(object): self.dict.pop(key, None) self.callables.pop(key, None) - def modified_event(self, attr, should_copy, previous, passive=False): + def modified_event(self, attr, should_copy, previous, passive=PASSIVE_OFF): needs_committed = attr.key not in self.committed_state if needs_committed: diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 473c71f7ed..87e35eb831 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -495,8 +495,13 @@ class PropertyLoader(StrategizedProperty): def cascade_iterator(self, type_, state, visited_instances, halt_on=None): if not type_ in self.cascade: return + # only actively lazy load on the 'delete' cascade - passive = type_ != 'delete' or self.passive_deletes + if type_ != 'delete' or self.passive_deletes: + passive = attributes.PASSIVE_NO_INITIALIZE + else: + passive = attributes.PASSIVE_OFF + mapper = self.mapper.primary_mapper() instances = state.value_as_iterable(self.key, passive=passive) if instances: diff --git a/test/orm/cascade.py b/test/orm/cascade.py index 5d4e64f0bf..56337b9ba4 100644 --- a/test/orm/cascade.py +++ b/test/orm/cascade.py @@ -15,10 +15,13 @@ class O2MCascadeTest(_fixtures.FixtureTest): def setup_mappers(self): mapper(Address, addresses) mapper(User, users, properties = dict( - addresses = relation(Address, cascade="all, delete-orphan"), + addresses = relation(Address, cascade="all, delete-orphan", backref="user"), orders = relation( mapper(Order, orders), cascade="all, delete-orphan") )) + mapper(Dingaling,dingalings, properties={ + 'address':relation(Address) + }) @testing.resolve_artifact_names def test_list_assignment(self): @@ -120,6 +123,32 @@ class O2MCascadeTest(_fixtures.FixtureTest): [User(name='newuser', orders=[Order(description='someorder')])]) + @testing.resolve_artifact_names + def test_cascade_nosideeffects(self): + """test that cascade leaves the state of unloaded scalars/collections unchanged.""" + + sess = create_session() + u = User(name='jack') + sess.add(u) + assert 'orders' not in u.__dict__ + + sess.flush() + + assert 'orders' not in u.__dict__ + + a = Address(email_address='foo@bar.com') + sess.add(a) + assert 'user' not in a.__dict__ + a.user = u + sess.flush() + + d = Dingaling(data='d1') + d.address_id = a.id + sess.add(d) + assert 'address' not in d.__dict__ + sess.flush() + assert d.address is a + @testing.resolve_artifact_names def test_cascade_delete_plusorphans(self): sess = create_session()