From 7ca47975377e3c64c734cae946708a2bdfb665c2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 7 Dec 2007 16:13:19 +0000 Subject: [PATCH] - added "cascade delete" behavior to "dynamic" relations just like that of regular relations. if passive_deletes flag (also just added) is not set, a delete of the parent item will trigger a full load of the child items so that they can be deleted or updated accordingly. --- CHANGES | 5 ++++ lib/sqlalchemy/orm/__init__.py | 4 ++- lib/sqlalchemy/orm/dynamic.py | 45 +++++++++++++++++++++------------- test/orm/dynamic.py | 23 ++++++++++++++++- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/CHANGES b/CHANGES index 99a7edcff0..ae64f385cb 100644 --- a/CHANGES +++ b/CHANGES @@ -53,6 +53,11 @@ CHANGES have not, you might notice your apps using a lot fewer queries than before in some situations. [ticket:871] + - added "cascade delete" behavior to "dynamic" relations just like + that of regular relations. if passive_deletes flag (also just added) + is not set, a delete of the parent item will trigger a full load of + the child items so that they can be deleted or updated accordingly. + - query.get() and query.load() do not take existing filter or other criterion into account; these methods *always* look up the given id in the database or return the current instance from the identity map, diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 7f5672371b..7ce298f713 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -245,7 +245,8 @@ def relation(argument, secondary=None, **kwargs): return PropertyLoader(argument, secondary=secondary, **kwargs) def dynamic_loader(argument, secondary=None, primaryjoin=None, secondaryjoin=None, entity_name=None, - foreign_keys=None, backref=None, post_update=False, cascade=None, remote_side=None, enable_typechecks=True): + foreign_keys=None, backref=None, post_update=False, cascade=None, remote_side=None, enable_typechecks=True, + passive_deletes=False): """construct a dynamically-loading mapper property. This property is similar to relation(), except read operations @@ -263,6 +264,7 @@ def dynamic_loader(argument, secondary=None, primaryjoin=None, secondaryjoin=Non return PropertyLoader(argument, secondary=secondary, primaryjoin=primaryjoin, secondaryjoin=secondaryjoin, entity_name=entity_name, foreign_keys=foreign_keys, backref=backref, post_update=post_update, cascade=cascade, remote_side=remote_side, enable_typechecks=enable_typechecks, + passive_deletes=passive_deletes, strategy_class=DynaLoader) #def _relation_loader(mapper, secondary=None, primaryjoin=None, secondaryjoin=None, lazy=True, **kwargs): diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index 0c49bcfc39..f5662a933d 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -1,8 +1,8 @@ """'dynamic' collection API. returns Query() objects on the 'read' side, alters a special AttributeHistory on the 'write' side.""" -from sqlalchemy import exceptions -from sqlalchemy.orm import attributes, object_session +from sqlalchemy import exceptions, util +from sqlalchemy.orm import attributes, object_session, util as mapperutil from sqlalchemy.orm.query import Query from sqlalchemy.orm.mapper import has_identity, object_mapper @@ -23,7 +23,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl): state.dict[self.key] = CollectionHistory(self, state) def get_collection(self, state, user_data=None): - return self.get_history(state)._added_items + return self.get_history(state, passive=True)._added_items def set(self, state, value, initiator): if initiator is self: @@ -39,19 +39,23 @@ class DynamicAttributeImpl(attributes.AttributeImpl): def get_history(self, state, passive=False): try: - return state.dict[self.key] + c = state.dict[self.key] except KeyError: state.dict[self.key] = c = CollectionHistory(self, state) + + if not passive: + return CollectionHistory(self, state, apply_to=c) + else: return c def append(self, state, value, initiator, passive=False): if initiator is not self: - self.get_history(state)._added_items.append(value) + self.get_history(state, passive=True)._added_items.append(value) self.fire_append_event(state, value, initiator) def remove(self, state, value, initiator, passive=False): if initiator is not self: - self.get_history(state)._deleted_items.append(value) + self.get_history(state, passive=True)._deleted_items.append(value) self.fire_remove_event(state, value, initiator) @@ -64,7 +68,7 @@ class AppenderQuery(Query): def __session(self): instance = self.state.obj() sess = object_session(instance) - if sess is not None and instance in sess and sess.autoflush: + if sess is not None and self.autoflush and sess.autoflush and instance in sess: sess.flush() if not has_identity(instance): return None @@ -74,14 +78,14 @@ class AppenderQuery(Query): def __iter__(self): sess = self.__session() if sess is None: - return iter(self.attr.get_history(self.state)._added_items) + return iter(self.attr.get_history(self.state, passive=True)._added_items) else: return iter(self._clone(sess)) def __getitem__(self, index): sess = self.__session() if sess is None: - return self.attr.get_history(self.state)._added_items.__getitem__(index) + return self.attr.get_history(self.state, passive=True)._added_items.__getitem__(index) else: return self._clone(sess).__getitem__(index) @@ -96,7 +100,7 @@ class AppenderQuery(Query): try: sess = object_mapper(instance).get_session() except exceptions.InvalidRequestError: - raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (self.instance.__class__, self.key)) + raise exceptions.InvalidRequestError("Parent instance %s is not bound to a Session, and no contextual session is established; lazy load operation of attribute '%s' cannot proceed" % (mapperutil.instance_str(instance), self.attr.key)) return sess.query(self.attr.target_mapper).with_parent(instance) @@ -106,7 +110,7 @@ class AppenderQuery(Query): oldlist = list(self) else: oldlist = [] - self.attr.get_history(self.state).replace(oldlist, collection) + self.attr.get_history(self.state, passive=True).replace(oldlist, collection) return oldlist def append(self, item): @@ -119,12 +123,19 @@ class AppenderQuery(Query): class CollectionHistory(attributes.AttributeHistory): """Overrides AttributeHistory to receive append/remove events directly.""" - def __init__(self, attr, state): - self._deleted_items = [] - self._added_items = [] - self._unchanged_items = [] - self._state = state - + def __init__(self, attr, state, apply_to=None): + if apply_to: + deleted = util.IdentitySet(apply_to._deleted_items) + added = apply_to._added_items + coll = AppenderQuery(attr, state).autoflush(False) + self._unchanged_items = [o for o in util.IdentitySet(coll) if o not in deleted] + self._added_items = apply_to._added_items + self._deleted_items = apply_to._deleted_items + else: + self._deleted_items = [] + self._added_items = [] + self._unchanged_items = [] + def replace(self, olditems, newitems): self._added_items = newitems self._deleted_items = olditems diff --git a/test/orm/dynamic.py b/test/orm/dynamic.py index fa8828c20a..096d945781 100644 --- a/test/orm/dynamic.py +++ b/test/orm/dynamic.py @@ -24,6 +24,21 @@ class DynamicTest(FixtureTest): assert [User(id=7, addresses=[Address(id=1, email_address='jack@bean.com')])] == q.filter(User.id==7).all() assert fixtures.user_address_result == q.all() + def test_backref(self): + mapper(Address, addresses, properties={ + 'user':relation(User, backref=backref('addresses', lazy='dynamic')) + }) + mapper(User, users) + + sess = create_session() + ad = sess.query(Address).get(1) + def go(): + ad.user = None + self.assert_sql_count(testbase.db, go, 1) + sess.flush() + u = sess.query(User).get(7) + assert ad not in u.addresses + def test_no_count(self): mapper(User, users, properties={ 'addresses':dynamic_loader(mapper(Address, addresses)) @@ -101,9 +116,15 @@ class FlushTest(FixtureTest): sess.delete(u.addresses[4]) sess.delete(u.addresses[3]) assert [Address(email_address='a'), Address(email_address='b'), Address(email_address='d')] == list(u.addresses) - + sess.delete(u) + + # u.addresses relation will have to force the load + # of all addresses so that they can be updated + sess.flush() sess.close() + + assert testbase.db.scalar(addresses.count(addresses.c.user_id != None)) ==0 @testing.fails_on('maxdb') def test_remove_orphans(self): -- 2.47.3