summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/sqlalchemy/orm/mapper.py16
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py29
-rw-r--r--test/orm/cascade.py55
3 files changed, 75 insertions, 25 deletions
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 4efa3f1e8..ae48f9e55 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -174,22 +174,10 @@ class Mapper(object):
self.logger.debug("(" + self.class_.__name__ + "|" + (self.entity_name is not None and "/%s" % self.entity_name or "") + (self.local_table and self.local_table.description or str(self.local_table)) + (not self.non_primary and "|non-primary" or "") + ") " + msg)
def _is_orphan(self, obj):
- optimistic = has_identity(obj)
for (key,klass) in self.delete_orphans:
- if attributes.has_parent(klass, obj, key, optimistic=optimistic):
- return False
- else:
- if self.delete_orphans:
- if not has_identity(obj):
- raise exceptions.FlushError("instance %s is an unsaved, pending instance and is an orphan (is not attached to %s)" %
- (
- obj,
- ", nor ".join(["any parent '%s' instance via that classes' '%s' attribute" % (klass.__name__, key) for (key,klass) in self.delete_orphans])
- ))
- else:
- return True
- else:
+ if attributes.has_parent(klass, obj, key, optimistic=has_identity(obj)):
return False
+ return bool(self.delete_orphans)
def get_property(self, key, resolve_synonyms=False, raiseerr=True):
"""return a MapperProperty associated with the given key."""
diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py
index 48b6ea4cb..ae55b4c94 100644
--- a/lib/sqlalchemy/orm/unitofwork.py
+++ b/lib/sqlalchemy/orm/unitofwork.py
@@ -23,7 +23,7 @@ import StringIO, weakref
from sqlalchemy import util, logging, topological, exceptions
from sqlalchemy.orm import attributes, interfaces
from sqlalchemy.orm import util as mapperutil
-from sqlalchemy.orm.mapper import object_mapper, _state_mapper
+from sqlalchemy.orm.mapper import object_mapper, _state_mapper, has_identity
# Load lazily
object_session = None
@@ -37,23 +37,25 @@ class UOWEventHandler(interfaces.AttributeExtension):
self.key = key
self.class_ = class_
self.cascade = cascade
+
+ def _target_mapper(self, obj):
+ prop = object_mapper(obj).get_property(self.key)
+ return prop.mapper
def append(self, obj, item, initiator):
# process "save_update" cascade rules for when an instance is appended to the list of another instance
sess = object_session(obj)
if sess:
if self.cascade.save_update and item not in sess:
- mapper = object_mapper(obj)
- prop = mapper.get_property(self.key)
- ename = prop.mapper.entity_name
- sess.save_or_update(item, entity_name=ename)
+ sess.save_or_update(item, entity_name=self._target_mapper(obj).entity_name)
def remove(self, obj, item, initiator):
sess = object_session(obj)
if sess:
# expunge pending orphans
if self.cascade.delete_orphan and item in sess.new:
- sess.expunge(item)
+ if self._target_mapper(obj)._is_orphan(item):
+ sess.expunge(item)
def set(self, obj, newvalue, oldvalue, initiator):
# process "save_update" cascade rules for when an instance is attached to another instance
@@ -62,10 +64,7 @@ class UOWEventHandler(interfaces.AttributeExtension):
sess = object_session(obj)
if sess:
if newvalue is not None and self.cascade.save_update and newvalue not in sess:
- mapper = object_mapper(obj)
- prop = mapper.get_property(self.key)
- ename = prop.mapper.entity_name
- sess.save_or_update(newvalue, entity_name=ename)
+ sess.save_or_update(newvalue, entity_name=self._target_mapper(obj).entity_name)
if self.cascade.delete_orphan and oldvalue in sess.new:
sess.expunge(oldvalue)
@@ -210,7 +209,15 @@ class UnitOfWork(object):
if state in processed:
continue
- flush_context.register_object(state, isdelete=_state_mapper(state)._is_orphan(state.obj()))
+ obj = state.obj()
+ is_orphan = _state_mapper(state)._is_orphan(obj)
+ if is_orphan and not has_identity(obj):
+ raise exceptions.FlushError("instance %s is an unsaved, pending instance and is an orphan (is not attached to %s)" %
+ (
+ obj,
+ ", nor ".join(["any parent '%s' instance via that classes' '%s' attribute" % (klass.__name__, key) for (key,klass) in _state_mapper(state).delete_orphans])
+ ))
+ flush_context.register_object(state, isdelete=is_orphan)
processed.add(state)
# put all remaining deletes into the flush context.
diff --git a/test/orm/cascade.py b/test/orm/cascade.py
index e2b2c6122..1abf01de7 100644
--- a/test/orm/cascade.py
+++ b/test/orm/cascade.py
@@ -415,6 +415,61 @@ class UnsavedOrphansTest2(ORMTest):
assert items.count().scalar() == 0
assert attributes.count().scalar() == 0
+class UnsavedOrphansTest3(ORMTest):
+ """test not expuning double parents"""
+
+ def define_tables(self, meta):
+ global sales_reps, accounts, customers
+ sales_reps = Table('sales_reps', meta,
+ Column('sales_rep_id', Integer, Sequence('sales_rep_id_seq'), primary_key = True),
+ Column('name', String(50)),
+ )
+ accounts = Table('accounts', meta,
+ Column('account_id', Integer, Sequence('account_id_seq'), primary_key = True),
+ Column('balance', Integer),
+ )
+ customers = Table('customers', meta,
+ Column('customer_id', Integer, Sequence('customer_id_seq'), primary_key = True),
+ Column('name', String(50)),
+ Column('sales_rep_id', Integer, ForeignKey('sales_reps.sales_rep_id')),
+ Column('account_id', Integer, ForeignKey('accounts.account_id')),
+ )
+
+ def test_double_parent_expunge(self):
+ """test that removing a pending item from a collection expunges it from the session."""
+ class Customer(fixtures.Base):
+ pass
+ class Account(fixtures.Base):
+ pass
+ class SalesRep(fixtures.Base):
+ pass
+
+ mapper(Customer, customers)
+ mapper(Account, accounts, properties=dict(
+ customers=relation(Customer, cascade="all,delete-orphan", backref="account")
+ ))
+ mapper(SalesRep, sales_reps, properties=dict(
+ customers=relation(Customer, cascade="all,delete-orphan", backref="sales_rep")
+ ))
+ s = create_session()
+
+ a = Account(balance=0)
+ sr = SalesRep(name="John")
+ [s.save(x) for x in [a,sr]]
+ s.flush()
+
+ c = Customer(name="Jane")
+
+ a.customers.append(c)
+ sr.customers.append(c)
+ assert c in s
+
+ a.customers.remove(c)
+ assert c in s, "Should not expunge customer yet, still has one parent"
+
+ sr.customers.remove(c)
+ assert c not in s, "Should expunge customer when both parents are gone"
+
class DoubleParentOrphanTest(ORMTest):
"""test orphan detection for an entity with two parent relations"""