Re: [Sqlalchemy-tickets] [sqlalchemy] #2741: uselist=False collection not backref deferred, leads t
Brought to you by:
zzzeek
|
From: sqlalchemy <mi...@zz...> - 2013-06-04 16:30:41
|
#2741: uselist=False collection not backref deferred, leads to unwanted
load/autoflush
---------------------------+-------------------------------
Reporter: zzzeek | Owner: zzzeek
Type: defect | Status: new
Priority: highest | Milestone: 0.8.xx
Component: orm | Severity: major - 1-3 hours
Resolution: | Keywords:
Progress State: in queue |
---------------------------+-------------------------------
Comment (by zzzeek):
the difference between uselist=True and uselist=False is that the old
value must be *removed*. Plenty of tests in test_backref_mutations test
this kind of thing. Suppose we disable the load on backref:
{{{
#!diff
diff --git a/lib/sqlalchemy/orm/attributes.py
b/lib/sqlalchemy/orm/attributes.py
index bfba695..4ff6261 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -784,7 +784,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl):
if initiator and initiator.parent_token is self.parent_token:
return
- if self.dispatch._active_history:
+ if passive is PASSIVE_OFF and self.dispatch._active_history:
old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
else:
old = self.get(state, dict_, passive=PASSIVE_NO_FETCH)
}}}
then run the test like this:
{{{
#!python
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class A(Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
b = relationship("B", uselist=False, backref="a")
class B(Base):
__tablename__ = 'b'
id = Column(Integer, primary_key=True)
a_id = Column(Integer, ForeignKey('a.id'), nullable=False)
e = create_engine("sqlite://", echo='debug')
Base.metadata.create_all(e)
sess = Session(e)
a1 = A()
b1 = B(a=a1)
a2 = A()
b2 = B(a=a2)
sess.add_all([a1, b1, a2, b2])
sess.commit()
b1.a = a2
sess.commit()
print a2.b
}}}
`a2.b` now has *two* b's attached and `b2` has not been replaced by `b1`,
we get:
{{{
SELECT b.id AS b_id, b.a_id AS b_a_id
FROM b
WHERE ? = b.a_id
2013-06-04 12:28:23,543 INFO sqlalchemy.engine.base.Engine (2,)
2013-06-04 12:28:23,543 DEBUG sqlalchemy.engine.base.Engine Col ('b_id',
'b_a_id')
2013-06-04 12:28:23,543 DEBUG sqlalchemy.engine.base.Engine Row (1, 2)
2013-06-04 12:28:23,543 DEBUG sqlalchemy.engine.base.Engine Row (2, 2)
/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/strategies.py:524:
SAWarning: Multiple rows returned with uselist=False for lazily-loaded
attribute 'A.b'
return self._emit_lazyload(session, state, ident_key, passive)
<__main__.B object at 0x10161f7d0>
}}}
in this case, if we stick to the plan and do no_autoflush:
{{{
with sess.no_autoflush:
b1.a = a2
sess.commit()
print a2.b
}}}
we still get an integrity error, because the replacement of `b2` throws a
not null exception, but this is what we want in this case, since
uselist=False means the old value must be replaced.
--
Ticket URL: <http://www.sqlalchemy.org/trac/ticket/2741#comment:1>
sqlalchemy <http://www.sqlalchemy.org/>
The Database Toolkit for Python
|