Issue 1:
Migrated issue, originally created by Aidan Kane (@aidankane)

I'm seeing slightly surprising behaviour around loading options when an entity is already in the session identity_map. I've attached a self-contained test case so you can more easily see what I mean.

Depending on how you query for an entity (get() vs .filter().one()) you get different loading behaviour if the entity is already in the identity_map.

In the simplest case:

x = session.query(A).get(1)

# the loading options are ignored here
x = session.query(A).options(...loading_options...).get(1)

# while they're honoured here
x = session.query(A).options(...loading_options...).filter(A.id==1).one()

To run my test case:

import loading_behaviour_bug
loading_behaviour_bug.setup()

# subquery loading works
loading_behaviour_bug.test_expected1()

# subquery loading works
loading_behaviour_bug.test_expected2()

# subquery loading doesn't work
loading_behaviour_bug.test_unexpected()

I suspect this might be expected behaviour but I couldn't find anything about it in the docs.

Whenever you call get() it's coming from the identity_map, and it may have different children than those in the db (because another bit of code has made a change / dirtied the object) - though I would have thought that would be ok because it's in the same transaction.

My issue is that depending on what code has run up to this point I can't use .get() because I can't trust the eager loading to run (my current workaround is to use the filter().one() pattern in critical bits of code). It might that I just need a little more management around the scope of the unit-or-work but you can see how easy it would be to get this wrong (especially in a web application where the recommendation is to tie the unit-of-work to the entire request).

Then again, I may have just completely missed something because it seems implausible to me that I could find anything resembling a bug in SQLA :)

Attachments: loading_behaviour_bug.py

Issue 2:
Migrated issue, originally created by Michael Bayer (@zzzeek)

this patch begins to provide this behavior. However a lazyloader from an expired object emits a second unnecessary load (or an unnecessary join, depending on how you look at it), and polymorphic loading breaks entirely, probably due to an incompatibility with the partial joined inheritance loader.

diff -r cfdc41c75ce2e2228d71733212bbaca0fe17ce35 lib/sqlalchemy/orm/state.py
--- a/lib/sqlalchemy/orm/state.py	Mon Apr 05 13:16:29 2010 -0400
+++ b/lib/sqlalchemy/orm/state.py	Mon Apr 05 15:43:40 2010 -0400
@@ -262,9 +262,10 @@
if kw.get('passive') is attributes.PASSIVE_NO_FETCH:
return attributes.PASSIVE_NO_RESULT

- toload = self.expired_attributes.\
- intersection(self.unmodified)
-
+ toload = self.expired_attributes.intersection(self.unmodified).union(
+ self.unloaded.intersection(self.expire_on_missing)
+ )
+
self.manager.deferred_scalar_loader(self, toload)

# if the loader failed, or this
@@ -277,6 +278,10 @@
return ATTR_WAS_SET

@property
+ def expire_on_missing(self):
+ return set(key for key in self.manager if self.manager[key](key).impl.expire_missing)
+
+ @property
def unmodified(self):
"""Return the set of keys which have no uncommitted changes"""

diff -r cfdc41c75ce2e2228d71733212bbaca0fe17ce35 test/orm/test_expire.py
--- a/test/orm/test_expire.py	Mon Apr 05 13:16:29 2010 -0400
+++ b/test/orm/test_expire.py	Mon Apr 05 15:43:40 2010 -0400
@@ -425,17 +425,7 @@
assert len(u.addresses) == 2

@testing.resolve_artifact_names
- def test_joinedload_props_dontload(self):
- # relationships currently have to load separately from scalar instances.
- # the use case is: expire "addresses". then access it. lazy load
- # fires off to load "addresses", but needs foreign key or primary key
- # attributes in order to lazy load; hits those attributes, such as
- # below it hits "u.id". "u.id" triggers full unexpire operation,
- # joinedloads addresses since lazy='joined'. this is all wihtin lazy load
- # which fires unconditionally; so an unnecessary joinedload (or
- # lazyload) was issued. would prefer not to complicate lazyloading to
- # "figure out" that the operation should be aborted right now.
-
+ def test_joinedload_props_load(self):
mapper(User, users, properties={
'addresses':relationship(Address, backref='user', lazy='joined'),
})
@@ -444,11 +434,30 @@
u = sess.query(User).get(8)
sess.expire(u)
u.id
- assert 'addresses' not in u.__dict__
+ assert 'addresses' in u.__dict__
u.addresses
assert 'addresses' in u.__dict__

@testing.resolve_artifact_names
+ def test_joinedload_props_load_two(self):
+ mapper(User, users, properties={
+ 'addresses':relationship(Address, backref='user', lazy='joined'),
+ })
+ mapper(Address, addresses)
+ sess = create_session()
+ u = sess.query(User).get(8)
+ sess.expire(u)
+
+ # current bug: u.addresses unexpires attributes,
+ # eagerlaods u.addresses. lazyloader for u.addresses
+ # then runs anyway.
+ def go():
+ u.addresses
+ assert 'addresses' in u.__dict__
+ assert 'id' in u.__dict__
+ self.assert_sql_count(testing.db, go, 1)
+
+ @testing.resolve_artifact_names
def test_expire_synonym(self):
mapper(User, users, properties={
'uname': sa.orm.synonym('name')