Hi all,

I'm having problems with relationships getting out of sync with the 
underlying foreign key properties, and hoping that someone can help.

It seems from the manual (if I've understood correctly), that deleting one 
side of a relationship should set foreign keys on the other side to 
NULL/None:

if our User.addresses relationship does *not* have delete cascade, 
SQLAlchemy’s default behavior is to instead de-associate address1 and 
address2 from user1 by setting their foreign key reference to NULL.


However this does not appear to extend to relationship properties, which 
remain set to the old values even after a flush.

Here is some example code:


from sqlalchemy import Column, FetchedValue, ForeignKey, Integer, MetaData, 
String, Table, create_engine, event
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relationship, sessionmaker

Base = declarative_base()

class House(Base):
    __tablename__ = "house"

    id = Column('id', Integer, primary_key=True)
    name = Column(String)

    dogs = relationship('Dog', backref=backref("house", uselist=False))

    def __repr__(self):
        return "House({})".format(self.name)

class Cat(Base):
    ''' Cat has a many-to-many relationship via a secondary table, but 
constrained to be unique and single-valued
        on both sides.
    '''
    __tablename__ = "cat"

    id = Column('id', Integer, primary_key=True)
    name = Column(String)

    house_cat = Table("house_cat", Base.metadata,
                      Column("id_house", Integer, ForeignKey(House.id), 
primary_key=True),
                      Column("id_cat", Integer, ForeignKey(id), 
unique=True))

    house = relationship(House, secondary=house_cat, uselist=False, 
backref=backref("cat", uselist=False))

    def __repr__(self):
        return "Cat({})".format(self.name)

class Dog(Base):
    ''' Dog has a foreign key and a many-to-one to House, with a 
one-to-many backref. '''
    __tablename__ = "dog"

    id = Column('id', Integer, primary_key=True)
    name = Column(String)
    id_house = Column(Integer, ForeignKey(House.id), unique=True)

    # house = relationship(House, uselist=False, backref=backref("dogs"))

    def __repr__(self):
        return "Dog({})".format(self.name)


engine = create_engine('sqlite://')
Session = sessionmaker(bind=engine)
Base.metadata.create_all(engine)

session = Session()
# session.bind.echo = True

def debug(desc, boolean, left, right):
    print "{:<40} {:<10} {:<20} {:<20}".format(desc, str(boolean), left, 
right)

bedsit = House(name='bedsit')
snoopy = Dog(name='snoopy', house=bedsit)
session.add(bedsit)
session.flush()
debug("bedsit.dogs == [snoopy]", bedsit.dogs == [snoopy], bedsit.dogs, 
[snoopy])
debug("snoopy.id_house == bedsit.id", snoopy.id_house == bedsit.id, 
snoopy.id_house, bedsit.id)
debug("snoopy.house is bedsit", snoopy.house is bedsit, snoopy.house, 
bedsit)

session.delete(snoopy)
debug("bedsit.dogs == []", bedsit.dogs == [], bedsit.dogs, [])

flat = House(name='flat')
muffin = Dog(name='muffin', house=flat)
session.add(flat)
session.flush()
debug("flat.dogs == [muffin]", flat.dogs == [muffin], flat.dogs, [muffin])
debug("muffin.id_house == flat.id", muffin.id_house == flat.id, 
muffin.id_house, flat.id)
debug("muffin.house == flat", muffin.house == flat, muffin.house, flat)

session.delete(flat)
session.flush()
debug("muffin.id_house is None", muffin.id_house is None, muffin.id_house, 
None)
debug("muffin.house is None", muffin.house is None, muffin.house, None)

mansion = House(name='mansion')
koshka = Cat(name='koshka', house=mansion)
session.add(mansion)
session.flush()
debug("mansion.cat is koshka", mansion.cat is koshka, mansion.cat, koshka)
debug("koshka.house == mansion", koshka.house == mansion, koshka.house, 
mansion)

session.delete(koshka)
debug("mansion.cat is None", mansion.cat is None, mansion.cat, None)

session.rollback()

This prints (I hope this preformatted table comes out OK for everyone):

Assertion                     Passes  Actual value    Expected value
=========                     ======  ============    ==============
bedsit.dogs == [snoopy]       True    [Dog(snoopy)]   [Dog(snoopy)] 

snoopy.id_house == bedsit.id  True    1               1 

snoopy.house is bedsit        True    House(bedsit)   House(bedsit) 

bedsit.dogs == []             False   [Dog(snoopy)]   [] 

flat.dogs == [muffin]         True    [Dog(muffin)]   [Dog(muffin)] 

muffin.id_house == flat.id    True    2               2 

muffin.house == flat          True    House(flat)     House(flat)   

muffin.id_house is None       True    None            None 

muffin.house is None          False   House(flat)     None 

mansion.cat is koshka         True    Cat(koshka)     Cat(koshka) 

koshka.house == mansion       True    House(mansion)  House(mansion) 

mansion.cat is None           False   Cat(koshka)     None

Ideally (for me :)) the second column would be True in all cases, 
indicating that the relationship has been synchronised with the underlying 
FK property. This is causing a problem for me because I delete a related 
object inside a transaction; later on I check whether the primary object 
has a related object and delete it if so; and if I end up processing the 
same object again, I get an error because the primary object refers to a 
deleted object, and deleting it again tries to delete rows (from the 
secondary table) that are already deleted:

    self.transaction.__exit__(a_type, value, traceback)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\session.py",
 
line 490, in __exit__

self.rollback()

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\util\langhelpers.py",
 
line 60, in __exit__

compat.reraise(exc_type, exc_value, exc_tb)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\session.py",
 
line 487, in __exit__

self.commit()

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\session.py",
 
line 392, in commit

self._prepare_impl()

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\session.py",
 
line 372, in _prepare_impl

self.session.flush()

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\session.py",
 
line 2004, in flush

self._flush(objects)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\session.py",
 
line 2122, in _flush

transaction.rollback(_capture_exception=True)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\util\langhelpers.py",
 
line 60, in __exit__

compat.reraise(exc_type, exc_value, exc_tb)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\session.py",
 
line 2086, in _flush

flush_context.execute()

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\unitofwork.py",
 
line 373, in execute

rec.execute(self)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\unitofwork.py",
 
line 485, in execute

self.dependency_processor.process_deletes(uow, states)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\dependency.py",
 
line 1023, in process_deletes

secondary_update, secondary_delete)

File 
"R:\sw\external\20151014-0\python27\lib\site-packages\sqlalchemy-1.0.8-py2.7-win32.egg\sqlalchemy\orm\dependency.py",
 
line 1111, in _run_crud

result.rowcount)

StaleDataError: DELETE statement on table 'blah' expected to delete 2 
row(s); Only 0 were matched.

I can just assign None to the relationship attribute after deleting the 
related object, but it seems to me that this will trip people up quite 
often and unexpectedly, and that SQLAlchemy would be easier to use if it 
was automatic.

Even better would be if deleting the related object immediately cleared the 
relationship (even before a flush), and assigning to an FK column 
immediately updated the relationship property and vice versa, but these may 
be outside of the design goals of SQLAlchemy. In some cases I could 
implement this manually, using an event listener on the FK column to update 
the relationship immediately, and vice versa, but again it seems that it 
would make SQLAlchemy easier to use if it was automatic.

I can see that there are synchronized properties which null out the FK 
column on the other side of a relationship when an object is deleted (and 
this is flushed to the database), but no synchronised properties for the 
relationship property. I looked at adding one of these for synchronising 
relationships, but it's quite complex with all the edge cases.

Can anyone see a better way of doing this, or a reason why it should not be 
done at all? Thanks in advance for your help!

Chris.

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to