Unsaved, pending instance and is an orphan problem

99 views
Skip to first unread message

Victor Lin

unread,
Jul 24, 2009, 12:45:13 AM7/24/09
to sqlalchemy
Hi,

I am writing a turbogears2 application, with elixir. I got a problem
when I try to create an new entity and attach it to another entity. I
create a simple program to repruduce the problem:

from elixir import *

class User(Entity):
name = Field(Unicode)
site = OneToOne('Site', cascade="all, delete-orphan")

class Site(Entity):
user = ManyToOne('User')
title = Field(Unicode)

metadata.bind = 'sqlite:///'
setup_all(True)

# we do create a new user victor here
victor = User(name=u'victor')
session.commit()

# and we create a site for victor
coolSite = Site(user=victor, title=u'Cool site')
session.commit()

or it can be download here : http://utilitybase.com/paste/15927

this is the trace back

Traceback (most recent call last):
File "I:\workspace\radio\radiosite\new_website\radioweb\radioweb
\model\show_bug.py", line 25, in <module>
coolSite = Site(user=victor, title=u'Cool site')
File "c:\python25\lib\site-packages\Elixir-0.6.1-py2.5.egg\elixir
\entity.py", line 718, in __call__
return type.__call__(cls, *args, **kwargs)
File "<string>", line 4, in __init__
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 890, in initialize_instance
return manager.events.original_init(*mixed[1:], **kwargs)
File "c:\python25\lib\site-packages\Elixir-0.6.1-py2.5.egg\elixir
\entity.py", line 876, in __init__
self.set(**kwargs)
File "c:\python25\lib\site-packages\Elixir-0.6.1-py2.5.egg\elixir
\entity.py", line 880, in set
setattr(self, key, value)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 151, in __set__
self.impl.set(instance_state(instance), value, None)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 557, in set
value = self.fire_replace_event(state, value, old, initiator)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 577, in fire_replace_event
value = ext.set(state, value, previous, initiator or self)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 810, in set
new_state.get_impl(self.key).append(new_state, state.obj(),
initiator, passive=PASSIVE_NO_CALLABLES)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 387, in append
self.set(state, value, initiator)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 556, in set
old = self.get(state)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\attributes.py",
line 375, in get
value = callable_()
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\strategies.py",
line 589, in __call__
result = q.all()
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\query.py", line
1193, in all
return list(self)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\query.py", line
1286, in __iter__
self.session._autoflush()
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\session.py", line
899, in _autoflush
self.flush()
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\session.py", line
1351, in flush
self._flush(objects)
File "C:\Python25\Lib\site-packages\sqlalchemy\orm\session.py", line
1408, in _flush
mapperutil.state_str(state), path))
sqlalchemy.orm.exc.FlushError: Instance <Site at 0x10ad310> is an
unsaved, pending instance and is an orphan (is not attached to any
parent 'User' instance via that classes' 'site' attribute)

It is so strange, why it said that my site is a orphan? It seems that
there is something behind that method invoke autoflush, but at this
moment, the site is not attached to user yet, so here comes the
problem?

The version of sqlalchemy is 5.5, and the version of elixir is 0.6.1.
I don't understand, the user is already there in memory, what will
invoke a autoflush? Is that a bug of sqlalchemy?
It is an unreasonable behavior sqlalchemy did, it stop me from add any
site to my user. How can I solve this problem?

Thanks.
Victor Lin.

Michael Bayer

unread,
Jul 24, 2009, 10:14:59 AM7/24/09
to sqlal...@googlegroups.com
Victor Lin wrote:
>
> Hi,
>
> I am writing a turbogears2 application, with elixir. I got a problem
> when I try to create an new entity and attach it to another entity. I
> create a simple program to repruduce the problem:
>
> from elixir import *
>
> class User(Entity):
> name = Field(Unicode)
> site = OneToOne('Site', cascade="all, delete-orphan")
>
> class Site(Entity):
> user = ManyToOne('User')
> title = Field(Unicode)
>
> metadata.bind = 'sqlite:///'
> setup_all(True)
>
> # we do create a new user victor here
> victor = User(name=u'victor')
> session.commit()
>
> # and we create a site for victor
> coolSite = Site(user=victor, title=u'Cool site')
> session.commit()
>

elixir is adding the "Site" object to the session before the "user=victor"
attribute is set. The setting of user=victor in turn is triggering an
attribute operation on User.site, which is a result of the bidirectional
reference between User and Site. All attributes on User have been
expired since you called session.commit(), and upon next access will be
re-loaded from the database.

If User.sites were one-to-many (more common), SQLA would be stashing the
value in a temporary collection and would not be triggering the load,
however in the case of a scalar reference (User.site), it triggers a load
in order to create a history event. Since Site is already in the session,
the session first flushes any pending changes before loading, and the
error is raised.

It is a TODO within SQLAlchemy for this particular operation to not
trigger an unnecessary load.

However, the biggest issue here is that Elixir is automatically adding
User to the Session, which is a behavior SQLAlchemy has been discouraging
for a couple of years now as it leads to more than one scenario where this
kind of thing happens, so you should check on the Elixir list for how to
disable this.

As far as the loading and the flushing is concerned, that is the result of
the session's expire_on_commit and autoflush behavior both of which are
described in the session documentation.

Michael Bayer

unread,
Jul 24, 2009, 10:33:36 AM7/24/09
to sqlal...@googlegroups.com
Michael Bayer wrote:

>
> Victor Lin wrote:
>>
> It is a TODO within SQLAlchemy for this particular operation to not
> trigger an unnecessary load.
>


I've added a patch as well as your test as
http://www.sqlalchemy.org/trac/ticket/1483 to address the unnecessary load
issue. It needs more testing to ensure things don't go wrong, but feel
free to apply said patch to your 0.5.5 copy if you want to experiment.
I'll try to test further this weekend.

Michael Bayer

unread,
Jul 25, 2009, 8:10:47 PM7/25/09
to sqlal...@googlegroups.com

OK , somewhat bad news on this front, the test you have needs to do
what its doing. Here's why:

first, make a User with a Site:

v = User(name='victor')
s1 = Site(title='s1')
v.site = s1
sess.add(v)
sess.commit()

everything is fine there. the commit() expires everything.

now make another site, and hook it up.

s2 = Site(title='asdf', user=v)
sess.add(s2)
sess.commit()

when you associate s2 with v, the ORM *must* locate the previous "s1"
row from the database, since you've set this up as one-to-one. the
previous "s1" row needs to be deleted and the ORM only knows that
because the action of setting s2.user hits the User.site backref,
which properly loads the previous row and queues it up for deletion.

the usual use case for "delete-orphan" cascade is on a many-to-one,
its unusual for it to be set on the non-foreign key side of a one-to-
one.

so while I still may do something with 1483, this specific case can't
change.

Reply all
Reply to author
Forward
0 new messages