Django Quick Tips #1.5: Manager Methods

May 18, 2007

In Django Quick Tips #1 I briefly discussed how using custom managers helps reduce the amount of code you’ll write. This time, however, we’ll not have a second manager but rather have extra methods in a custom manager.

For our example we’ll use a simple blog post:

class Post(models.Model):
    title = models.CharField(maxlength=100)
    text = models.TextField()
    date_published = models.DateTimeField()
    is_published = models.BooleanField(default=False)

From this model we want to say that “published” posts have is_published is True and date_published is less than or equal to now. For this we’ll create a manager called PostManager and create a live() method.

class PostManager(models.Manager):
    def live(self):
        return self.filter(is_published=True, 
            date_published__lte=datetime.datetime.now())

We make this our default manager by adding this line to our Post model:

objects = PostManager()

Now we can use the manager like normal or use our new method:

Post.objects.all()  # Returns all posts
Post.objects.live() # Returns only "published" blog posts

In use with a generic view

Generic views let you access your data quickly and easily. Normally you’d set your post_dict like so:

post_dict = {'queryset':Post.objects.all()}

Instead, we’ll use live():

post_dict = {'queryset':Post.objects.live()}

Now we don’t have to create a view just to filter specific objects.

Let’s say I have a related blog object in my template from which I want to use this method. This might look like:

{% for post in blog.post_set.live %}
    {{ post.title }}
{% endfor %}

In conclusion…

Django’s managers are a brilliant way to have specific views of your data. Adding a second manager doesn’t seem to be as DRY as having extra methods in your primary manager.

A get_object_or_404 caveat

The often-used shortcut function get_object_or_404() allows you to pass a specific manager, but it won’t work with manager methods:

# This works
p = get_object_or_404(Post.published_objects, pk=1)
# This does not
p = get_object_or_404(Post.objects.live(), pk=1)

This is because get_object_or_404() expects a model manager as the first argument. In my humble opinion, this function should be appended to accept a queryset.


Comments

Nice example, except that checking date_published > now renders the DB query cache useless (at least in MySQL), as now is different in each query. A workaround is to round the time in seconds by 100 (using floor instead of ceil) so that the query does not change for at least 100 seconds, and your DB is not swamped by queries when the page cache expires.

Posted by ludo

...or you do the caching at another layer than the DB.

I agree that get_object_or_404 should be extended - I run in to that all the time.

Posted by SmileyChris

As an update: I've submitted a patch extending get_object_or_404 and get_list_or_404 to accept QuerySets.

http://code.djangoproject.com/ticket/...

Posted by SuperJared

Question: Why do you make 'objects = PostManager()' the default Manager? Shouldn't the 'objects = models.Manager()' be the default Manager listed ahead of a 'live_objects = PostManager()' (as you do in Quick Tip #1) so that queries like 'post.objects.all()' returns all posts?

Thanks.

Posted by Osiris

Osiris: In this post, I'm only extending the default manager with a new method, live()-- this does not affect any other method in the manager, like all(), get(), etc.

The previous post overwrites the initial QuerySet of the manager, so all(), get(), etc are all affected.

Either way works fine, but I believe this way is more DRY since you can chain these methods, and you don't have to worry about multiple managers.

Let's say I want a specific live post:

Old way: Post.published_objects.get(pk=1)
New way: Post.objects.live().get(pk=1)

Posted by SuperJared

Got it! Thank you. From the docs: "You can override a Manager‘s base QuerySet by overriding the Manager.get_query_set() method."

Great post.

Posted by Osiris

Just a quick note - that should be datetime.now instead of datetime.now(). We ran into caching problems on our site ( http://www.witigonen.com ) because of that. At least, in the current svn version :)

Posted by Michael

This is somewhat limited approach. To chain QuerySet methods you'd need to subclass Django's default QuerySet class. If you do

SomeModel.object.live()

it works fine.. but try:

SomeModel.object.filter(pk=1).live()

and it will fail, because Django returns a QuerySet from filter(). And because the new live()-filter is not in QuerySet but in manager, Django can't find it.

Posted by herion

Herion,

You can simply do this:

Post.objects.live().filter(pk=123)

Posted by SuperJared

Add your comment

No HTML; Only URLs and line breaks are converted.