Dear list,

I have an SQLAlchemy problem that has ruined my last weekend. So I have 
reduced the problem to the bare minimum and thought I'd ask here on what my 
fault is. :)

Imagine a (web) application that has Users (employees) and Items (on the 
shelf). Like in an online shop. Now there's a logbook that records every 
change that happens over time. Like a User has taken an Item and done 
something to it. So a logbook entry is both connected (many-to-one) to the 
Users and the Items. So I can find out what a certain User did 
(User.logbookentries) or see what has happened to a certain Item 
(Item.logbookentries). In the reverse way I can see which User and which 
Item a logbookentry refers to.

Allow me to show you my example code (you should be able to run it like 
that and immediately reproduce the problem):

=============================================
#!/usr/bin/env python                              
# -*- coding: utf-8 -*-                            

import sqlalchemy as sql
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy import MetaData, create_engine, orm    

Session = scoped_session(sessionmaker())
metadata = MetaData()                   

# Define the tables
users_table = sql.Table(
    'users', metadata,  
    sql.Column('id', sql.Integer, primary_key=True),
    sql.Column('name', sql.Unicode()),              
    )                                               

items_table = sql.Table(
    'items', metadata,  
    sql.Column('id', sql.Integer, primary_key=True),
    sql.Column('name', sql.Unicode),                
    )                                               

logbook_table = sql.Table(
    'logbook', metadata,  
    sql.Column('id', sql.Integer, primary_key=True),
    sql.Column('item_id', sql.Integer, sql.ForeignKey('items.id')),
    sql.Column('user_id', sql.Integer, sql.ForeignKey('users.id')),
    sql.Column('text', sql.Unicode(100)),                          
    )                                                              

# Define the classes for ORM mapping
class User(object): pass
class Item(object): pass
class LogbookEntry(object): pass

# ORM mapping
orm.mapper(User, users_table,
    properties={
        'logbookentries':orm.relation(LogbookEntry,
            # either works without this backref:
            backref=orm.backref('user', uselist=False),
            # or works if this becomes cascade="all,delete-orphan":
            ),
        }
    )

orm.mapper(Item, items_table,
    properties={
        'logbookentries':orm.relation(LogbookEntry,
                cascade="all, delete-orphan"),
        }
    )

orm.mapper(LogbookEntry, logbook_table)

# Connect to the database
engine = create_engine('sqlite:///mystuff.sqlite', echo=True)
Session.configure(bind=engine)

# Create database schema
metadata.create_all(bind=engine)

# Create a user
user = User()
user.name=u'SomeUser'
Session.save(user)

# Create an item
item = Item()
item.name=u'SomeItem'
Session.save(item)

# Save the previously created objects into the database
Session.commit()

# Create a logbook entry
logbookentry = LogbookEntry()
logbookentry.text = u'SomeLogText'

# Connect the LogbookEntry instance to the User and Item
logbookentry.user = user
item.logbookentries.append(logbookentry)

Session.commit()
=============================================

What actually happens when I run this code is this error message:

sqlalchemy.orm.exc.FlushError: Instance <LogbookEntry at 0x91f5b2c> is an 
unsaved, pending instance and is an orphan (is not attached to any parent 
'Item' instance via that classes' 'logbookentries' attribute)

I wasn't sure why this happens. After all I have connected the LogbookEntry 
to the Item by saying "item.logbookentries.append(logbookentry)" so the 
LogbookEntry isn't orphaned at all.

There were two remedies that made this code working. First one was to 
remove the line

            backref=orm.backref('user', uselist=False),

from the User.logbookentries mapper. And the second one was defining the 
cascade as

        cascade="all,delete-orphan"

instead of

        cascade="all"

Does SQLAlchemy want to tell me that a backref wouldn't work unless I 
enforce a user entry to be there by using a delete cacade from LogbookEntry 
to User? After all if I delete a User then the backref would point nowhere.

The reason I don't use 'delete-orphan' here is that a User (employee) could 
get fired and deleting the "User" from the database would automatically kill 
all the "LogbookEntry"s. But I want to preserve the logbook even if the 
referring User is gone. I would expect the logbook.user_id to be None if 
the User is gone.

The funny fact is that this code works well with SQLAlchemy 0.4.8 but fails 
on 0.5.3. And even if SQLAlchemy wants to save me from doing something 
illogical I had expected another error message telling me that my cascade 
without "delete-orphan" is useless if I want to have a backref on the 
'user'.

Is this a case of an improvable error message? Or have I just misunderstood 
it? After a lot of cursing I'd love to get your feedback on my issue. 
Thanks.

Kindly
 Christoph

Attachment: signature.asc
Description: This is a digitally signed message part.

Reply via email to