From: Mike Bayer Date: Fri, 18 Jul 2008 22:11:22 +0000 (+0000) Subject: - save-update and delete-orphan cascade event handler X-Git-Tag: rel_0_5beta3~33 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=100c17229fbc61cbc1276615b0d21043a8b85176;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - save-update and delete-orphan cascade event handler now considers the cascade rules of the event initiator only, not the local attribute. This way the cascade of the initiator controls the behavior regardless of backref events. --- diff --git a/CHANGES b/CHANGES index 13b9a979be..b3a95beeb8 100644 --- a/CHANGES +++ b/CHANGES @@ -9,6 +9,14 @@ CHANGES "0.4.7". - orm + - Cascade rules can now function unidirectionally on an + otherwise bidirectional relation(), taking only + the cascade behavior of the operation's initiator into + account. This means that a cascade of "none" on one + side won't trigger a "save-update" operation when an element + is attached to that relation, even if the backref + does indicate "save-update" cascade. + - Added a new SessionExtension hook called after_attach(). This is called at the point of attachment for objects via add(), add_all(), delete(), and merge(). diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 72d527e62c..7eecc33202 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -306,7 +306,6 @@ class AbstractRelationLoader(LoaderStrategy): uselist=self.uselist, useobject=True, extension=attribute_ext, - cascade=self.parent_property.cascade, trackparent=True, typecallable=self.parent_property.collection_class, callable_=callable_, diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index b4e649262a..ac73f979dd 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -36,10 +36,8 @@ class UOWEventHandler(interfaces.AttributeExtension): session cascade operations. """ - def __init__(self, key, class_, cascade): + def __init__(self, key): self.key = key - self.class_ = class_ - self.cascade = cascade def _target_mapper(self, state): prop = _state_mapper(state).get_property(self.key) @@ -49,14 +47,16 @@ class UOWEventHandler(interfaces.AttributeExtension): # process "save_update" cascade rules for when an instance is appended to the list of another instance sess = _state_session(state) if sess: - if self.cascade.save_update and item not in sess: + initiating_property = initiator.class_manager[initiator.key].property + if initiating_property.cascade.save_update and item not in sess: sess.save_or_update(item, entity_name=self._target_mapper(state).entity_name) def remove(self, state, item, initiator): sess = _state_session(state) if sess: + initiating_property = initiator.class_manager[initiator.key].property # expunge pending orphans - if self.cascade.delete_orphan and item in sess.new: + if initiating_property.cascade.delete_orphan and item in sess.new: if self._target_mapper(state)._is_orphan(attributes.instance_state(item)): sess.expunge(item) @@ -66,9 +66,10 @@ class UOWEventHandler(interfaces.AttributeExtension): return sess = _state_session(state) if sess: - if newvalue is not None and self.cascade.save_update and newvalue not in sess: + initiating_property = initiator.class_manager[initiator.key].property + if newvalue is not None and initiating_property.cascade.save_update and newvalue not in sess: sess.save_or_update(newvalue, entity_name=self._target_mapper(state).entity_name) - if self.cascade.delete_orphan and oldvalue in sess.new: + if initiating_property.cascade.delete_orphan and oldvalue in sess.new: sess.expunge(oldvalue) @@ -77,13 +78,12 @@ def register_attribute(class_, key, *args, **kwargs): to new InstrumentedAttributes. """ - cascade = kwargs.pop('cascade', None) useobject = kwargs.get('useobject', False) if useobject: # for object-holding attributes, instrument UOWEventHandler # to process per-attribute cascades extension = util.to_list(kwargs.pop('extension', None) or []) - extension.insert(0, UOWEventHandler(key, class_, cascade=cascade)) + extension.insert(0, UOWEventHandler(key)) kwargs['extension'] = extension return attributes.register_attribute(class_, key, *args, **kwargs) diff --git a/test/orm/cascade.py b/test/orm/cascade.py index 4df4559468..10f3cbf387 100644 --- a/test/orm/cascade.py +++ b/test/orm/cascade.py @@ -155,6 +155,99 @@ class O2MCascadeTest(_fixtures.FixtureTest): assert users.count().scalar() == 1 assert orders.count().scalar() == 0 +class NoSaveCascadeTest(_fixtures.FixtureTest): + """test that backrefs don't force save-update cascades to occur + when they're not desired in the forwards direction.""" + + @testing.resolve_artifact_names + def test_unidirectional_cascade_o2m(self): + mapper(Order, orders) + mapper(User, users, properties = dict( + orders = relation( + Order, cascade="none", backref="user") + )) + + sess = create_session() + + o1 = Order() + sess.add(o1) + u1 = User(orders=[o1]) + assert u1 not in sess + assert o1 in sess + + sess.clear() + u1 = User() + sess.add(u1) + o1 = Order() + o1.user = u1 + assert u1 in sess + assert o1 in sess + + @testing.resolve_artifact_names + def test_unidirectional_cascade_m2o(self): + mapper(Order, orders, properties={ + 'user':relation(User, cascade="none", backref="orders") + }) + mapper(User, users) + + sess = create_session() + + o1 = Order() + sess.add(o1) + o1.user = u1 = User() + assert u1 not in sess + assert o1 in sess + + sess.clear() + + o1 = Order() + sess.add(o1) + u1 = User(orders=[o1]) + assert u1 in sess + assert o1 in sess + + sess.clear() + + o1 = Order() + o1.user = u1 = User() + sess.add(o1) + assert u1 not in sess + assert o1 in sess + + sess.clear() + + o1 = Order() + u1 = User(orders=[o1]) + sess.add(u1) + assert u1 in sess + assert o1 in sess + + @testing.resolve_artifact_names + def test_unidirectional_cascade_m2m(self): + mapper(Item, items, properties={ + 'keywords':relation(Keyword, secondary=item_keywords, cascade="none", backref="items") + }) + mapper(Keyword, keywords) + + sess = create_session() + + i1 = Item() + k1 = Keyword() + sess.add(i1) + i1.keywords.append(k1) + assert i1 in sess + assert k1 not in sess + + sess.clear() + + i1 = Item() + k1 = Keyword() + sess.add(i1) + k1.items.append(i1) + assert i1 in sess + assert k1 in sess + + class O2MCascadeNoOrphanTest(_fixtures.FixtureTest): run_inserts = None