Django’s Admin in Production

Django’s Admin is amazing. A built-in and fully functional interface that quickly gets in and allows data entry is priceless. Developers can focus on building additional functionality instead of creating dummy interfaces to interact with the database. Not to say class-based views aren’t magical on their own; the Admin is simply quicker to configure.

All the good stuff aside, sometimes the Admin can be a bit dumb. Specifically, it tends to handle relationships poorly. There are two common ways that the Admin views can grind to a halt and several things that can be done to fix it.

Massive ForeignKey Relationships

By default Django will display all the options when editing a model that contains a ForeignKey relationship. Take the following Model structure for this example.

from django.db import models
class Foo (models.Model):
    name = models.CharField(max_length=100)

class Bar (models.Model):
    foo_parent = models.ForeignKey("Foo")
    name = models.CharField(max_length=100)

Suppose there are 1,000,000 Foo objects and you are editing a Bar object. Django will attempt to fetch all the Foo objects to render the select field. Not only is this incredibly cumbersome in terms of editing but it grinds to a halt at a certain point. This is an easy trap to fall into, especially when you have a Model specifically for data points, such as page view analytics.

This problem is further exposed when you do something silly, such as adding a ForeignKey field to the list_editable list. Now with only 100 Foo objects and 100 Bar objects you would generate a $O(n)$ queries to render a list view of $n$ Foo objects. Django won’t cache the queryset and reuse it across all the objects.

Recommendations

  1. Override the get_queryset method for the ModelAdmin class and use select_related or prefetch_related to avoid repeated queries.1
  2. Add the field to raw_id_fields to render a normal input with the id of the object instead of a select widget.
  3. If the upper bound of a ForeignKey field is not small enough to manage, add it to readonly_fields in the admin configuration and devise another method for editing it.
  4. Never add a ForeignKey field to list_editable without a very good reason and a very small related object set.
  5. Keep close attention to the number of queries it takes to render the list, add, and change views for the admin using something like Django Debug Toolbar.
  6. Use an alternative admin theme such as Grappelli that includes extra widgets such as autocomplete lookups to remove the requirement to query all related models before rendering the view.

Related Lookups

Django by default uses the __str__ (or __unicode__ if you’re using Python <3) method to display what a ForeignKey’s “value” is. Expanding on the example above:

from django.db import models

class Foo (models.Model):
    name = models.CharField(max_length=100)
    def __str__ (self):
        return self.name

class Bar (models.Model):
    foo = models.ForeignKey(Foo)
    subject = models.CharField(max_length=100)
    def __str__ (self):
        return "{} - {}".format(self.foo, self.subject)

class Caz (models.Model):
    bar = models.ForeignKey(bar)
    classifier = models.CharField(max_length=100)
    def __str__ (self):
        return "{}: {}".format(self.bar, self.classifier)

See anything obviously wrong yet? The recursive __str__ calling should be a big hint. What happens when the Admin tries to render a list of Caz objects that are listed as a ForeignKey from some other model? After grabbing a list of all Cazs, it starts iterating through them, which triggers a lookup on Bar, which triggers a lookup on Foo, for every single Caz object. This makes the number of queries required to render a simple select option take $O(2n)$ queries where $n$ is the number of Caz objects. This can easily cripple an edit view in production where some model count is exploding out of control.

Django's chained querying

Recommendations

  1. Avoid including ForeignKey fields in __str__ or __unicode__ methods for other Models.
  2. Use __repr__ instead for dumping helpful information out when prowling around in the shell.
  3. Define a custom property for outputting full object information in templates where the query count is manageable.
  4. Cache the related names on the object itself and have it update on save. This is non-normal form but will help control the number of queries generated automatically.
  5. Use raw SQL code to generate the information you need. The above for a list view could easily be replaced by some JOINs in a query.

Comment on reddit’s /r/django.


  1. Hat tip to spookylukey↩︎

written August 28th, 2014 and last updated September 2nd, 2014

August 2014

Can’t find what you’re looking for? Try hitting the home page or viewing all archives.