jQuery style chaining with the Django ORM
1st May 2008
Django’s ORM is, in my opinion, the unsung gem of the framework. For the subset of SQL that’s used in most web applications it’s very hard to beat. It’s a beautiful piece of API design, and I tip my hat to the people who designed and built it.
Lazy evaluation
If you haven’t spent much time with the ORM, two key features are lazy evaluation and chaining. Consider the following statement:
entries = Entry.objects.all()
Assuming you have created an Entry model of some sort, the above statement will create a Django QuerySet object representing all of the entries in the database. It will not result in the execution of any SQL—QuerySets are lazily evaluated, and are only executed at the last possible moment. The most common situation in which SQL will be executed is when the object is used for iteration:
for entry in entries:
print entry.title
This usually happens in a template:
<ul>
{% for entry in entries %}
<li>{{ entry.title }}</li>
{% endfor %}
</ul>
Lazy evaluation works nicely with template fragment caching—even if you pass a QuerySet to a template it won’t be executed if the fragment it is used in can be served from the cache.
You can modify QuerySets as many times as you like before they are executed:
entries = Entry.objects.all()
today = datetime.date.today()
entries_this_year = entries.filter(
posted__year = today.year
)
entries_last_year = entries.filter(
posted__year = today.year - 1
)
Again, no SQL has been executed, but we now have two QuerySets which, when iterated, will produce the desired result.
Chaining
Chaining comes in when you want to apply multiple modifications to a QuerySet. Here are blog entries from 2006 that weren’t posted in January:
Entry.objects.filter(
posted__year = 2006
).exclude(posted__month = 1)
And here’s entries from that year posted to the category named “Personal”, ordered by title:
Entry.objects.filter(
posted__year = 2006
).filter(
category__name = "Personal"
).order_by('title')
The above can also be expressed like this:
Entry.objects.filter(
posted__year = 2006,
category__name = "Personal"
).order_by('title')
Chaining in jQuery
The parallels to jQuery are pretty clear. The jQuery API is built around chaining, and the jQuery animation library even uses a form of lazy evaluation to automatically queue up effects to run in sequence:
jQuery('div#message').addClass(
'borderfade'
).animate({
'borderWidth': '+10px'
}, 1000).fadeOut();
One of the neatest things about jQuery is the plugin model, which takes advantage of JavaScript’s prototype inheritance and makes it trivially easy to add new chainable methods. If we wanted to package the above dumb effect up as a plugin, we could do so like this:
jQuery.fn.dumbBorderFade = function() {
return this.addClass(
'borderfade'
).animate({
'borderWidth': '+10px'
}, 1000).fadeOut();
};
Now we can apply it to an element like so:
jQuery('div#message').dumbBorderFade();
Custom QuerySet methods in Django
Django supports adding custom methods for accessing the ORM through the ability to implement a custom Manager. In the above examples, Entry.objects
is the Manager. The downside of this approach is that methods added to a manager can only be used at the beginning of the chain.
Luckily, Managers also provide a hook for returning a custom QuerySet. This means we can create our own QuerySet subclass and add new methods to it, in a way that’s reminiscent of jQuery:
from django.db import models
from django.db.models.query import QuerySet
import datetime
class EntryQuerySet(QuerySet):
def on_date(self, date):
next = date + datetime.timedelta(days = 1)
return self.filter(
posted__gt = date,
posted__lt = next
)
class EntryManager(models.Manager):
def get_query_set(self):
return EntryQuerySet(self.model)
class Entry(models.Model):
...
objects = EntryManager()
The above gives us a new method on the QuerySets returned by Entry.objects called on_date(), which lets us filter entries down to those posted on a specific date. Now we can run queries like the following:
Entry.objects.filter(
category__name = 'Personal'
).on_date(datetime.date(2008, 5, 1))
Reducing the boilerplate
This method works fine, but it requires quite a bit of boilerplate code—a QuerySet subclass and a Manager subclass plus the wiring to pull them all together. Wouldn’t it be neat if you could declare the extra QuerySet methods inside the model definition itself?
It turns out you can, and it’s surprisingly easy. Here’s the syntax I came up with:
from django.db.models.query import QuerySet
class Entry(models.Model):
...
objects = QuerySetManager()
...
class QuerySet(QuerySet):
def on_date(self, date):
return self.filter(
...
)
Here I’ve made the custom QuerySet class an inner class of the model definition. I’ve also replaced the default manager with a QuerySetManager. All this class does is return the QuerySet inner class for the current model from get_query_set. The implementation looks like this:
class QuerySetManager(models.Manager):
def get_query_set(self):
return self.model.QuerySet(self.model)
I’m pretty happy with this; it makes it trivial to add custom QuerySet methods and does so without any monkeypatching or deep reliance on Django ORM internals. I think the ease with which this can be achieved is a testament to the quality of the ORM API.
More recent articles
- Notes from Bing Chat—Our First Encounter With Manipulative AI - 19th November 2024
- Project: Civic Band - scraping and searching PDF meeting minutes from hundreds of municipalities - 16th November 2024
- Qwen2.5-Coder-32B is an LLM that can code well that runs on my Mac - 12th November 2024