Solving "n+1 query problem" in Django using custom template tags

Here is a neat trick I saw in Pinax, in the bookmarks app.

Related votes for bookmarks are retrieved in one query inside the custom template tag, and then grouped into dictionary using bookmark ids as keys. Then “get_item_from_dict” is used in the “for loop” to retrieve votes related to particular bookmark and display them in the template.

Generally speaking, if you need to select related items and can’t use “select_related”, you should seriously consider using django-batch-select. It handles both ForeignKey and ManyToManyField relations, and allows you to filter related models by their fields as well.

But it in some cases custom template tags feels like a more natural thing to do. In my case I wanted also to use “autopaginate” tag. So I wasn’t sure how django-batch-select would play with it, since pagination is happening in the template. Additional bonus is that there is no need to override manager on your model.

The only problem is that you need to write custom template tag, and get into subclassing Node, and parsing your tag arguments. Luckily we can use awesome django-templatetag-sugar written by Alex Gaynor. Writing custom Django tags has never been easier.

Assuming we have following models:

class Deal(models.Model):
    title = models.CharField(max_length=55)

class Coupon(models.Model):
     deal = models.ForeignKey(Deal)
     unique_id = models.CharField(max_length=55)

Our deals_tags.py would be something like following.

from django import template
from deals.models import Deal, Coupon

from templatetag_sugar.register import tag
from templatetag_sugar.parser import Name, Variable, Constant
from itertools import groupby

register = template.Library()

@tag(register, [Variable(), Constant("as"), Name()])
def related_coupons(context, qs, asvar):
    ids = map(lambda x: x.id, qs)
    coupons = Coupon.objects.filter(deal__in=ids)

    groups=[]
    uniquekeys=[]
    # groupby returns an iterator with iterators inside
    # so we need to go through each item and make a list from it
    for k, g in groupby(coupons, lambda y: y.deal_id):
        groups.append(list(g))
        uniquekeys.append(k)
    context[asvar] = dict(zip(uniquekeys, groups))
    return ''

@tag(register, [Variable(), Variable(), Constant("as"), Name()])
def get_from_dict(context, dict_var, key_var, asvar):
    context[asvar] = dict_var.get(key_var, None)
    return ''

Now you can use related coupons in your template:

{% related_coupons deals as coupons_dict %}

{% for deal in deals %}
    {{ deal.title }}
    {% get_from_dict coupons_dict deal.id as coupons %}
    # access your coupons here
{% endfor %}
Filed under  //   django  

About


Twitter