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:




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>

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)

Reply via email to