<p dir="auto"><strong>Migrated issue, originally created by Aidan Kane (<a href="https://github.com/aidankane">@aidankane</a>)</strong></p>
<p dir="auto">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.</p>
<p dir="auto">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.</p>
<p dir="auto">In the simplest case:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="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()
"><pre class="notranslate"><code class="notranslate">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()

</code></pre></div>
<p dir="auto">To run my test case:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="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()
"><pre class="notranslate"><code class="notranslate">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()

</code></pre></div>
<p dir="auto">I suspect this might be expected behaviour but I couldn't find anything about it in the docs.</p>
<p dir="auto">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.</p>
<p dir="auto">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).</p>
<p dir="auto">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 :)</p>
<hr>
<p dir="auto">Attachments: <a href="../wiki/imported_issue_attachments/3187/loading_behaviour_bug.py">loading_behaviour_bug.py</a></p>
<p dir="auto"><strong>Migrated issue, originally created by Michael Bayer (<a href="https://github.com/zzzeek">@zzzeek</a>)</strong></p>
<p dir="auto">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.</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="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):
         &quot;&quot;&quot;Return the set of keys which have no uncommitted changes&quot;&quot;&quot;
         
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 &quot;addresses&quot;.  then access it.  lazy load
-        # fires off to load &quot;addresses&quot;, but needs foreign key or primary key
-        # attributes in order to lazy load; hits those attributes, such as
-        # below it hits &quot;u.id&quot;.  &quot;u.id&quot; 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
-        # &quot;figure out&quot; 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')"><pre class="notranslate"><code class="notranslate">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')
</code></pre></div>