tags:

views:

644

answers:

3

Say I have 2 models:

class Poll(models.Model):
    category = models.CharField(u"Category", max_length = 64)
    [...]

class Choice(models.Model):
    poll = models.ForeignKey(Poll)
    [...]

Given a Poll object, I can query its choices with:

poll.choice_set.all()

But, is there a utility function to query all choices from a set of Poll?

Actually, I'm looking for something like the following (which is not supported, and I don't seek how it could be):

polls = Poll.objects.filter(category = 'foo').select_related('choice_set')
for poll in polls:
    print poll.choice_set.all() # this shouldn't perform a SQL query at each iteration

I made an (ugly) function to help me achieve that:

def qbind(objects, target_name, model, field_name):
    objects = list(objects)
    objects_dict = dict([(object.id, object) for object in objects])
    for foreign in model.objects.filter(**{field_name + '__in': objects_dict.keys()}):
        id = getattr(foreign, field_name + '_id')
        if id in objects_dict:
            object = objects_dict[id]
            if hasattr(object, target_name):
                getattr(object, target_name).append(foreign)
            else:
                setattr(object, target_name, [foreign])
    return objects

which is used as follow:

polls = Poll.objects.filter(category = 'foo')
polls = qbind(polls, 'choices', Choice, 'poll')
# Now, each object in polls have a 'choices' member with the list of choices.
# This was achieved with 2 SQL queries only.

Is there something easier already provided by Django? Or at least, a snippet doing the same thing in a better way.

How do you handle this problem usually?

+6  A: 

I think what you're saying is, "I want all Choices for a set of Polls." If so, try this:

polls = Poll.objects.filter(category='foo')
choices = Choice.objects.filter(poll__in=polls)
a paid nerd
+1 I didn't know about this feature! How completely elegant!
David Berger
This is what I do at the beginning of the `qbind` function. But actually I want the set of choices *per* poll, not the whole set of choice. For example, if I want to display the list of polls on a template, along with choices for each of them, I do not want to hit the database for each poll. The point of the `qbind` function is to join together your `polls` and `choices` data to achieve that.
Frédéric Jolliton
A: 

I think what you are trying to do is the term "eager loading" of child data - meaning you are loading the child list (choice_set) for each Poll, but all in the first query to the DB, so that you don't have to make a bunch of queries later on.

If this is correct, then what you are looking for is 'select_related' - see http://docs.djangoproject.com/en/dev/ref/models/querysets/#id4

I noticed you tried 'select_related' but it didn't work. Can you try doing the 'select_related' and then the filter. That might fix it.


UPDATE: This doesn't work, see comments below.

NathanD
select_related would be useful if a was querying for Choice and wanted to preload each corresponding Poll. But here, I want the opposite which is not supported by select_related (think about the corresponding SQL query, it cannot be done in one query without duplicating lots of data.) This should be done with 2 queries.
Frédéric Jolliton
Yep, your right. Sorry for not seeing that. Since 'choice_set' is not available until the query is evaluated, it doesn't recognize that it even exists.
NathanD
+1  A: 

First answer: don't waste time writing something like qbind until you've already written a working application, profiled it, and demonstrated that N queries is actually a performance problem for your database and load scenarios.

But maybe you've done that. So second answer: qbind() does what you'll need to do, but it would be more idiomatic if packaged in a custom QuerySet subclass, with an accompanying Manager subclass that returns instances of the custom QuerySet. Ideally you could even make them generic and reusable for any reverse relation. Then you could do something like:

Poll.objects.filter(category='foo').fetch_reverse_relations('choices_set')

For an example of the Manager/QuerySet technique, see this snippet, which solves a similar problem but for the case of Generic Foreign Keys, not reverse relations. It wouldn't be too hard to combine the guts of your qbind() function with the structure shown there to make a really nice solution to your problem.

Carl Meyer