the detached error is because even though the "baked" query emits the correct SQL with the LEFT OUTER JOIN, there are callables that are present inside the QueryContext that are tailored to look specifically for a particular alias() of the mapped "Address" table, which is not the same alias() object that's in your "cached" query - so the joined eager loader for "User.address" looks in the row for its columns, sees that they're not there (since the ORM targets column rows by Column() object), and doesn't populate the "address" attribute. So the attribute remains unloaded until you access it where you get your detached error.
The "use labels" error that you got early on was due to the fact that the wiki recipe was for some silly reason using "self.statement" to get at the statement instead of the context.statement it just generated, not sure what that was about. The recipe on the wiki also has the issue that it isn't even caching anything to do with the QueryContext, including all of this information regarding eager joins which is pretty important. Your modifications try to correct for this by storing that "context", but then it still creates a brand new context anyway and just transfers not nearly enough of its state over for things to work. As the comment on the wiki suggested, I'm not seeing any issue if we just cache the whole QueryContext and then just use it again. But there's a few things we have to be careful of, one is that the QueryContext holds onto the Query and Session that it's related to, so we delete those before caching. Then, we make a shallow copy of it when we actually want to return a usable QueryContext and poke on the current Query/Session, and also copy the "attributes" dictionary just in case some loader wants to mess with things in there too (and for some reason there's a naming inconsistency with that dictionary too it seems I haven't fixed yet). Besides the Session being stuck on the QueryContext, there's a numeric counter called a "runid" that gets stuck onto it at loading time that should only be used once. So the whole thing is rolled up into the "named" thing I referred to also, so that there's no need to keep a Query object hanging around, when we say "bake()" we're really just referring to a position in the code somewhere, so I've updated the wiki recipe to use a named system like this: q = s.query(Foo).\ filter(Foo.data == bindparam('foo')).\ bake_as("foo", cache) result = q.params(foo='data 12').all() A highly cleaned up version of your test is attached. I'm still not sure I'm getting everything accounted for here! thanks for testing ! The feature is actually looking quite simple and probably works better as something built in, or at least if we added some methods to QueryContext to ease the burden of caching/copying it. On May 31, 2013, at 4:40 PM, Claudio Freire <klaussfre...@gmail.com> wrote: > > > > On Fri, May 31, 2013 at 4:47 PM, Claudio Freire <klaussfre...@gmail.com> > wrote: > > On Fri, May 31, 2013 at 4:44 PM, Michael Bayer <mike...@zzzcomputing.com> > wrote: > can you just attach a working .py script > > > How does that work without a database? > > > Ok, I took one of SQLA's tests, and make it break ;) > > Notice the problem here is that I close the session after querying. > > Since the baked query has a joinedload, it shouldn't matter, but it does, > because when baking, eager loads are broken somehow. > > > -- > You received this message because you are subscribed to the Google Groups > "sqlalchemy" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to sqlalchemy+unsubscr...@googlegroups.com. > To post to this group, send email to sqlalchemy@googlegroups.com. > Visit this group at http://groups.google.com/group/sqlalchemy?hl=en. > For more options, visit https://groups.google.com/groups/opt_out. > > > <test_baked.py>
-- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To unsubscribe from this group and stop receiving emails from it, send an email to sqlalchemy+unsubscr...@googlegroups.com. To post to this group, send email to sqlalchemy@googlegroups.com. Visit this group at http://groups.google.com/group/sqlalchemy?hl=en. For more options, visit https://groups.google.com/groups/opt_out.
On May 31, 2013, at 4:40 PM, Claudio Freire <klaussfre...@gmail.com> wrote:
|
from sqlalchemy import bindparam from sqlalchemy.orm import joinedload, Session, relationship from sqlalchemy.orm import query, mapper from sqlalchemy.testing import eq_ from test.orm import _fixtures from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql.expression import Select class BakedQuery(query.Query): _baked_context = None _baked_cache = None @query._generative() def bake_as(self, name, cache): """Freeze the statement used by this Query.""" if name not in cache: cache[name] = context = self._compile_context() del context.session del context.query self._baked_context = cache[name] self._baked_cache = cache def _compile_context(self, **kw): if self._baked_context is not None: context = query.QueryContext.__new__(query.QueryContext) context.__dict__.update(self._baked_context.__dict__) context.query = self context.session = self.session # need to fix these names, urg context.attributes = context._attributes = context.attributes.copy() return context else: return super(BakedQuery, self)._compile_context(**kw) def _execute_and_instances(self, querycontext): if self._baked_cache is not None: self = self.execution_options(compiled_cache=self._baked_cache) return super(BakedQuery, self)._execute_and_instances(querycontext) CacheableQuery = BakedQuery class EagerTest(_fixtures.FixtureTest): run_inserts = 'once' run_deletes = None def test_baked(self): # intercept compilations of Select to count them. compilations = [0] @compiles(Select) def visit_select(element, compiler, **kw): compilations[0] += 1 return compiler.visit_select(element, **kw) users, Address, addresses, User = (self.tables.users, self.classes.Address, self.tables.addresses, self.classes.User) mapper(Address, addresses) mapper(User, users, properties={ 'addresses': relationship(Address) }) sess = Session(query_cls=CacheableQuery) cache = {} for i in xrange(10): sess = Session(query_cls=CacheableQuery) q1 = sess.query(User).filter(User.id == bindparam('id')).options( joinedload(User.addresses)).bake_as("load_my_user", cache) # note that with this first() here, we normally add "LIMIT 1", # but because we are already baked, we're loading all the # rows, should there be more than one (most DBAPIs buffer rows). u1 = q1.params(id=7).first() assert u1.id == 7 assert "addresses" in u1.__dict__ eq_([a.id for a in u1.addresses], [1]) u2 = q1.params(id=8).first() eq_(u2.id, 8) assert "addresses" in u2.__dict__ eq_([a.id for a in u2.addresses], [2, 3, 4]) eq_(compilations[0], 1) eq_(len(cache), 2)