From: Mike Bayer Date: Sun, 3 Mar 2013 01:27:53 +0000 (-0500) Subject: Can set/change the "cascade" attribute on a :func:`.relationship` X-Git-Tag: rel_0_8_0~12^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b83dd4dc2200bece2896a125be6d4f0911669d15;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Can set/change the "cascade" attribute on a :func:`.relationship` construct after it's been constructed already. This is not a pattern for normal use but we like to change the setting for demonstration purposes in tutorials. --- diff --git a/doc/build/changelog/changelog_08.rst b/doc/build/changelog/changelog_08.rst index 37b0236268..370de26039 100644 --- a/doc/build/changelog/changelog_08.rst +++ b/doc/build/changelog/changelog_08.rst @@ -16,6 +16,14 @@ * :ref:`metadata_create_drop_tables` + .. change:: + :tags: feature, orm + + Can set/change the "cascade" attribute on a :func:`.relationship` + construct after it's been constructed already. This is not + a pattern for normal use but we like to change the setting + for demonstration purposes in tutorials. + .. change:: :tags: bug, schema :tickets: 2664 diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index b6391eba3a..b3f7baf0f9 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -149,7 +149,7 @@ class MapperProperty(_MappedAttribute, _InspectionAttr): """ - cascade = () + cascade = frozenset() """The set of 'cascade' attribute names. This collection is checked before the 'cascade_iterator' method is called. diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index c618e89b22..a0d8c92d1a 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -230,6 +230,8 @@ class RelationshipProperty(StrategizedProperty): strategy_wildcard_key = 'relationship:*' + _dependency_processor = None + def __init__(self, argument, secondary=None, primaryjoin=None, secondaryjoin=None, @@ -252,7 +254,7 @@ class RelationshipProperty(StrategizedProperty): load_on_pending=False, strategy_class=None, _local_remote_pairs=None, query_class=None, - info=None): + info=None): self.uselist = uselist self.argument = argument @@ -297,17 +299,8 @@ class RelationshipProperty(StrategizedProperty): self._reverse_property = set() - if cascade is not False: - self.cascade = CascadeOptions(cascade) - else: - self.cascade = CascadeOptions("save-update, merge") - - if self.passive_deletes == 'all' and \ - ("delete" in self.cascade or - "delete-orphan" in self.cascade): - raise sa_exc.ArgumentError( - "Can't set passive_deletes='all' in conjunction " - "with 'delete' or 'delete-orphan' cascade") + self.cascade = cascade if cascade is not False \ + else "save-update, merge" self.order_by = order_by @@ -723,8 +716,8 @@ class RelationshipProperty(StrategizedProperty): if self.property._use_get: return sql.and_(*[ sql.or_( - adapt(x) != state_bindparam(adapt(x), state, y), - adapt(x) == None) + adapt(x) != state_bindparam(adapt(x), state, y), + adapt(x) == None) for (x, y) in self.property.local_remote_pairs]) criterion = sql.and_(*[x == y for (x, y) in @@ -838,7 +831,7 @@ class RelationshipProperty(StrategizedProperty): if (source_state, r) in _recursive: return - if not "merge" in self.cascade: + if not "merge" in self._cascade: return if self.key not in source_dict: @@ -916,7 +909,7 @@ class RelationshipProperty(StrategizedProperty): def cascade_iterator(self, type_, state, dict_, visited_states, halt_on=None): - #assert type_ in self.cascade + #assert type_ in self._cascade # only actively lazy load on the 'delete' cascade if type_ != 'delete' or self.passive_deletes: @@ -933,7 +926,7 @@ class RelationshipProperty(StrategizedProperty): passive=passive) skip_pending = type_ == 'refresh-expire' and 'delete-orphan' \ - not in self.cascade + not in self._cascade for instance_state, c in tuples: if instance_state in visited_states: @@ -980,7 +973,7 @@ class RelationshipProperty(StrategizedProperty): 'does not reference mapper %s' % (key, self, other, self.parent)) if self.direction in (ONETOMANY, MANYTOONE) and self.direction \ - == other.direction: + == other.direction: raise sa_exc.ArgumentError('%s and back-reference %s are ' 'both of the same direction %r. Did you mean to ' 'set remote_side on the many-to-one side ?' @@ -1025,7 +1018,7 @@ class RelationshipProperty(StrategizedProperty): self._check_conflicts() self._process_dependent_arguments() self._setup_join_conditions() - self._check_cascade_settings() + self._check_cascade_settings(self._cascade) self._post_init() self._generate_backref() super(RelationshipProperty, self).do_init() @@ -1043,7 +1036,7 @@ class RelationshipProperty(StrategizedProperty): for attr in ( 'order_by', 'primaryjoin', 'secondaryjoin', 'secondary', '_user_defined_foreign_keys', 'remote_side', - ): + ): attr_value = getattr(self, attr) if util.callable(attr_value): setattr(self, attr, attr_value()) @@ -1080,10 +1073,6 @@ class RelationshipProperty(StrategizedProperty): self.target = self.mapper.mapped_table - if self.cascade.delete_orphan: - self.mapper.primary_mapper()._delete_orphans.append( - (self.key, self.parent.class_) - ) def _setup_join_conditions(self): self._join_condition = jc = relationships.JoinCondition( @@ -1134,29 +1123,58 @@ class RelationshipProperty(StrategizedProperty): if not self.parent.concrete: for inheriting in self.parent.iterate_to_root(): if inheriting is not self.parent \ - and inheriting.has_property(self.key): + and inheriting.has_property(self.key): util.warn("Warning: relationship '%s' on mapper " "'%s' supersedes the same relationship " "on inherited mapper '%s'; this can " "cause dependency issues during flush" % (self.key, self.parent, inheriting)) - def _check_cascade_settings(self): - if self.cascade.delete_orphan and not self.single_parent \ + @property + def cascade(self): + """Return the current cascade setting for this + :class:`.RelationshipProperty`. + """ + return self._cascade + + @cascade.setter + def cascade(self, cascade): + cascade = CascadeOptions(cascade) + if 'mapper' in self.__dict__: + self._check_cascade_settings(cascade) + self._cascade = cascade + + if self._dependency_processor: + self._dependency_processor.cascade = cascade + + def _check_cascade_settings(self, cascade): + if cascade.delete_orphan and not self.single_parent \ and (self.direction is MANYTOMANY or self.direction is MANYTOONE): raise sa_exc.ArgumentError( 'On %s, delete-orphan cascade is not supported ' - 'on a many-to-many or many-to-one relationship ' - 'when single_parent is not set. Set ' - 'single_parent=True on the relationship().' - % self) + 'on a many-to-many or many-to-one relationship ' + 'when single_parent is not set. Set ' + 'single_parent=True on the relationship().' + % self) if self.direction is MANYTOONE and self.passive_deletes: util.warn("On %s, 'passive_deletes' is normally configured " "on one-to-many, one-to-one, many-to-many " "relationships only." % self) + if self.passive_deletes == 'all' and \ + ("delete" in cascade or + "delete-orphan" in cascade): + raise sa_exc.ArgumentError( + "On %s, can't set passive_deletes='all' in conjunction " + "with 'delete' or 'delete-orphan' cascade" % self) + + if cascade.delete_orphan: + self.mapper.primary_mapper()._delete_orphans.append( + (self.key, self.parent.class_) + ) + def _columns_are_mapped(self, *cols): """Return True if all columns in the given collection are mapped by the tables referenced by this :class:`.Relationship`. @@ -1164,10 +1182,10 @@ class RelationshipProperty(StrategizedProperty): """ for c in cols: if self.secondary is not None \ - and self.secondary.c.contains_column(c): + and self.secondary.c.contains_column(c): continue if not self.parent.mapped_table.c.contains_column(c) and \ - not self.target.c.contains_column(c): + not self.target.c.contains_column(c): return False return True diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 98636d9358..1f5115c410 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -40,9 +40,9 @@ def track_cascade_events(descriptor, prop): prop = state.manager.mapper._props[key] item_state = attributes.instance_state(item) - if prop.cascade.save_update and \ + if prop._cascade.save_update and \ (prop.cascade_backrefs or key == initiator.key) and \ - not sess._contains_state(item_state): + not sess._contains_state(item_state): sess._save_or_update_state(item_state) return item @@ -63,9 +63,9 @@ def track_cascade_events(descriptor, prop): # expunge pending orphans item_state = attributes.instance_state(item) - if prop.cascade.delete_orphan and \ + if prop._cascade.delete_orphan and \ item_state in sess._new and \ - prop.mapper._is_orphan(item_state): + prop.mapper._is_orphan(item_state): sess.expunge(item) def set_(state, newvalue, oldvalue, initiator): @@ -83,14 +83,14 @@ def track_cascade_events(descriptor, prop): prop = state.manager.mapper._props[key] if newvalue is not None: newvalue_state = attributes.instance_state(newvalue) - if prop.cascade.save_update and \ + if prop._cascade.save_update and \ (prop.cascade_backrefs or key == initiator.key) and \ not sess._contains_state(newvalue_state): sess._save_or_update_state(newvalue_state) if oldvalue is not None and \ oldvalue is not attributes.PASSIVE_NO_RESULT and \ - prop.cascade.delete_orphan: + prop._cascade.delete_orphan: # possible to reach here with attributes.NEVER_SET ? oldvalue_state = attributes.instance_state(oldvalue) diff --git a/test/orm/test_cascade.py b/test/orm/test_cascade.py index 00d19e7928..12196b4e70 100644 --- a/test/orm/test_cascade.py +++ b/test/orm/test_cascade.py @@ -37,6 +37,22 @@ class CascadeArgTest(fixtures.MappedTest): class Address(cls.Basic): pass + def test_delete_with_passive_deletes_all(self): + User, Address = self.classes.User, self.classes.Address + users, addresses = self.tables.users, self.tables.addresses + + mapper(User, users, properties={ + 'addresses': relationship(Address, + passive_deletes="all", cascade="all, delete-orphan") + }) + mapper(Address, addresses) + assert_raises_message( + sa_exc.ArgumentError, + "On User.addresses, can't set passive_deletes='all' " + "in conjunction with 'delete' or 'delete-orphan' cascade", + configure_mappers + ) + def test_delete_orphan_without_delete(self): User, Address = self.classes.User, self.classes.Address users, addresses = self.tables.users, self.tables.addresses @@ -69,6 +85,33 @@ class CascadeArgTest(fixtures.MappedTest): orm_util.CascadeOptions("all, delete-orphan"), frozenset) + def test_cascade_assignable(self): + User, Address = self.classes.User, self.classes.Address + users, addresses = self.tables.users, self.tables.addresses + + rel = relationship(Address) + eq_(rel.cascade, set(['save-update', 'merge'])) + rel.cascade = "save-update, merge, expunge" + eq_(rel.cascade, set(['save-update', 'merge', 'expunge'])) + + mapper(User, users, properties={ + 'addresses': rel + }) + am = mapper(Address, addresses) + configure_mappers() + + eq_(rel.cascade, set(['save-update', 'merge', 'expunge'])) + + assert ("addresses", User) not in am._delete_orphans + rel.cascade = "all, delete, delete-orphan" + assert ("addresses", User) in am._delete_orphans + + eq_(rel.cascade, + set(['delete', 'delete-orphan', 'expunge', 'merge', + 'refresh-expire', 'save-update']) + ) + + class O2MCascadeDeleteOrphanTest(fixtures.MappedTest): run_inserts = None