why inconsistent orphan behavior for persistent vs. pending?

Issue #2655 resolved
Mike Bayer repo owner created an issue

Figure out the rationale behind the DoubleParentO2MOrphanTest regarding the auto-expunge. This patch reverses it:

diff -r b1df6fab53a0d740fe60f04e5c9ad01027ba59af lib/sqlalchemy/orm/unitofwork.py
--- a/lib/sqlalchemy/orm/unitofwork.py  Mon Jan 21 18:17:10 2013 -0500
+++ b/lib/sqlalchemy/orm/unitofwork.py  Tue Jan 22 20:55:19 2013 -0500
@@ -63,9 +63,11 @@

             # expunge pending orphans
             item_state = attributes.instance_state(item)
+
             if prop.cascade.delete_orphan and \
                 item_state in sess._new and \
-                prop.mapper._is_orphan(item_state):
+                not state.get_impl(key).hasparent(item_state):
+                #prop.mapper._is_orphan(item_state):
                     sess.expunge(item)

     def set_(state, newvalue, oldvalue, initiator):
@@ -95,7 +97,8 @@
                 oldvalue_state = attributes.instance_state(oldvalue)

                 if oldvalue_state in sess._new and \
-                    prop.mapper._is_orphan(oldvalue_state):
+                    not state.get_impl(key).hasparent(oldvalue_state):
+                    #prop.mapper._is_orphan(oldvalue_state):
                     sess.expunge(oldvalue)
         return newvalue

here's a test based on the association proxy example that seems quite reasonable:

from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship, backref

from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String(64))

    # association proxy of "user_keywords" collection
    # to "keyword" attribute
    keywords = association_proxy('user_keywords', 'keyword')

    def __init__(self, name):
        self.name = name

class UserKeyword(Base):
    __tablename__ = 'user_keyword'
    user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
    keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
    special_key = Column(String(50))

    # bidirectional attribute/collection of "user"/"user_keywords"
    user = relationship(User,
                backref=backref("user_keywords",
                                cascade="all, delete-orphan")
            )

    keyword = relationship("Keyword",
                backref=backref("user_keywords", cascade="all, delete-orphan"))

    def __init__(self, keyword=None, user=None, special_key=None):
        self.user = user
        self.keyword = keyword
        self.special_key = special_key

class Keyword(Base):
    __tablename__ = 'keyword'
    id = Column(Integer, primary_key=True)
    keyword = Column('keyword', String(64))

    users = association_proxy('user_keywords', 'user')

    def __init__(self, keyword):
        self.keyword = keyword

    def __repr__(self):
        return 'Keyword(%s)' % repr(self.keyword)

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
Base.metadata.drop_all(e)
Base.metadata.create_all(e)

session = Session(e)

rory = User("rory")
session.add(rory)
chicken = Keyword('chicken')
session.add(chicken)
rory.keywords.append(chicken)

# add this in, test passes, as the remove below
# triggers a single-parent orphan event.
# take it out, object is not expunged, get an integrity error.
#session.flush()

rory.keywords.remove(chicken)

session.flush()

clearly we did this expunge behavior for a reason. But I'm not seeing the wisdom of it at the moment, but this might be hard to change now. So not sure if we can do anything, perhaps document it, provide some backwards compatible workaround, not sure.

Comments (2)

  1. Log in to comment