OK, this is actually something people have asked for a lot.  in the  
beginning, recently, etc.  also for different reasons...i.e.  
convenience, or performance, etc.   So, first off let me start by  
illustrating how this use case is done right now.   Assuming your  
Address mapper has a backref "user" to the User mapper, its  just:

        for address in session.query(Address).filter_by(user=someuser).filter 
(address_table.c.postcode == 5000):
                print address.street

and of course, if you dont have the backref, then we go back to the  
regular SQL stuff:

        for address in session.query(Address).filter 
(user.id==someuser.id).filter(address_table.c.postcode == 5000):
                print address.street

in the second example, we are figuring out the "lazy" criterion  
ourselves.  in most cases this is pretty easy to figure out.  but we  
do have some exotic mappings these days, primarily because of  
polymorphic union queries, and it might not be as trivial to make up  
the join condition.  But,  the join conditions and bind params which  
have been calculated by "LazyLoader" are just sitting there, they can  
be currently pulled out with a little non-API attribute access but  
Ive no problem with adding some API-level accessors to get at the  
Query object calculated for a particular property (i.e. what you are  
using in your patch internally).

So if you want to take the above and make it all slick, all you have  
to do is stick the above query inside a property:

        class User(object):
                def _get_addresses(self):
                        return object_session(self).query(Address).filter 
(user.id==someuser.id)
                addresses = property(_get_addresses)

then you can just say pretty much what you want:

        for address in someuser.addresses.filter(address_table.c.postcode ==  
5000):
                print address.street

plus, the "addresses" object is a Query !  so you already have the  
full interface, and it has __iter__ and __getitem__() !  so the list  
operations on the base object work just fine (and they even work like  
SQLObjects)

        for address in someuser.addresses:   # __iter__() fires off the query !

        for address in someuser.addresses[3:5]  # slice applies OFFSET and  
LIMIT !
        
now lets look at the way your patch does it.

        addresses = user.addresses.filter(address_table.c.postcode == 5000)

seems easy.  right ?  remember that "user" is now in the session.   
anyone else that queries for "user" will get that same User  
instance.  but the rest of the app is going to assume normal  
relationship semantics on that collection....which means:

        print user.addresess   # <-- FAIL  - this code assumes the list is  
complete, its not, its already been loaded incompletely

        print someaddress in user.addresses # <-- FAIL - the address is not  
present

        user.addresses.remove(someaddress) # <-- ERROR - the address is not  
present

        user.addresses.insert(5, someotheraddress) # <-- FAIL - the list is  
incomplete, ordering will be incorrect

        session.flush()  # <-- FAIL - we have to figure out what items were  
added/removed/unchanged from the collection...but the data's  
incomplete !

This is an issue introduced by our usage of sessions, identity maps,  
etc., things that simpler ORMs dont have to worry about.  So thats  
the part of this I dont want to get into (and im hoping you dont,  
either).  Yes, the use case you want is desireable, and ive no  
problem adding in hooks to make it possible.  but No, I dont think it  
should be injected into the base attribute lazy-loading operation.    
i think the semantics of a "Query for related items" and that of a  
"mapped relationship" are just plain different, even though  
implementation-wise on the view side at least they are both "just a  
SQL query".  with the primary issue being that all mutating/uow- 
related operations go out the window with an incomplete collection.   
from a performance point of view,  the manipulation of huge  
collections could be achieved via loading "stub" objects which is  
something discussed long ago, which is how Hibernate addresses it  
(called "extra-lazy loading").  so if we ever did that, thats how we  
would do it (i.e. a complete collection of incomplete objects).

anyway back to the query side, Ive even been considering making folks  
happy with this and adding a new kind of mapped property called, i  
dont know...."query_relation()", "relquery()",  "lazyloader()",  
something like that.  and, right in the core, how often does that  
happen ?  it would copy the setup style of "relation()" but be a lot  
more view oriented.

        mapper(SomeClass, table, properties={
                'addresses':lazyloader(Address)
        })

and there you go !  does the same thing as the manual property code I  
illustrated does, returns a Query from which you can do whatever.

We also had a guy who wanted to do a more complex primaryjoin which  
was confusing the lazyloader.   I showed him how to manually tack on  
a lazy-callable.  but i want to stick that into lazyloader(),  
basically a "callable_" argument that fires off using the attribute's  
lazy-callable mechanism:

        def load_addresses(session, instance):
                return session.query(Address).filter 
(create_some_really_complicated_join_condition)

        mapper(SomeClass, table, properties={
                'addresses':lazyloader(callable_=load_addresses)
        })

the lazyloader() function above is doing almost nothing, its just  
tacking on whatever callable you'd like.   it can return a generative  
Query, Addresses, farm animals, whatever...just a convenient way to  
hook into the attribute manager's lazy-callable logic.

so as long as we can agree on the "its a read-only thing" aspect of  
this, we're good to go.  otherwise you have to define for me how all  
those mutating operations are going to work (and even then, its  
additional core complexity im not sure if i can add to my support-load).


On Mar 23, 2007, at 11:36 AM, Gaetan de Menten wrote:

> Hi list,
>
> The recent work that Michael has done about bringing SelectResults
> functionality to Query made me think: what if that allowed me to do
> what I've always wanted to have ever since I started using ORMs? The
> thing I've wanted to do is to have one-to-many relations that you can
> filter when you access them, and it generates the correct query behind
> the scene without fetching too many rows from the DB.
>
> So I dived into the code a bit deeper than I had up until now... In
> the end, I didn't use the SelectResults stuff at all, but I've managed
> to come up with a working patch to do what I wanted anyway. The most
> interesting part of it is the addition of a DeferredInstrumentedList
> which takes a callable as argument and actually call it only when the
> list is accessed.
>
> This allows to do stuff like that:
>
> for address in user.addresses.filter(address_table.c.postcode ==  
> 5000):
>      print address.street
>
> The patch is quite experimental (I'm pretty sure I broke some stuff).
> Also, it only includes a proof-of-concept filter method, but it should
> be almost trivial to add things like: limit, offset, order_by,
> distinct, list slicing and so on...
>
> And anyway, I'm not sure it's the correct way to go. I _think_ the
> best way to go would be if InstrumentedList would hold a
> "preconfigured" query object, that you could alter the same way you
> can alter the normal query objects, and which would only be executed
> when one access the list elements. But to get to this point would need
> a major refactoring of the code while what I've done is pretty simple.
>
> I also attach an (Elixir-made) example demonstrating the thing.
>
> Michael, if you are interested in this, I'll gladly translate the
> example into a plain SQLAlchemy unittest. And more generally, if there
> is anything I can do (within the limits of my knowledge of SQLAlchemy)
> to get this done the "correct" way and included into the trunk, please
> tell me. I'm so thrilled with this new toy, I hope it'll be possible
> to include it ! :)
>
> -- 
> Gaƫtan de Menten
> http://openhex.org
>
> >
> <deferred_list_relation.diff>
> from elixir import *
>
> class A(Entity):
>     has_field('name', String(30))
>     has_many('b', of_kind='B')
>
> class B(Entity):
>     has_field('name', String(30))
>     belongs_to('a', of_kind='A')
>     has_field('extra', Integer)
>
> metadata.connect('sqlite:///')
> create_all()
>
> a1 = A(name='a1')
> a2 = A(name='a2')
> b1 = B(name='b1', a=a1, extra=10)
> b2 = B(name='b2', a=a2)
> b3 = B(name='b3', a=a1, extra=5)
>
> objectstore.flush()
> objectstore.clear()
>
> metadata.engine.echo = True
> a = A.get_by(name='a1')
> print [b.name for b in a.b.filter(B.c.extra < 6)]
>


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To post to this group, send email to sqlalchemy@googlegroups.com
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/sqlalchemy?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to