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 <[email protected]> wrote:
>
>
>
> On Fri, May 31, 2013 at 4:47 PM, Claudio Freire <[email protected]>
> wrote:
>
> On Fri, May 31, 2013 at 4:44 PM, Michael Bayer <[email protected]>
> 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 [email protected].
> To post to this group, send email to [email protected].
> 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 [email protected]. To post to this group, send email to [email protected]. 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 <[email protected]> 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)
