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
- Override the
get_queryset
method for theModelAdmin
class and useselect_related
orprefetch_related
to avoid repeated queries.1 - Add the field to
raw_id_fields
to render a normal input with theid
of the object instead of a select widget. - If the upper bound of a
ForeignKey
field is not small enough to manage, add it toreadonly_fields
in the admin configuration and devise another method for editing it. - Never add a
ForeignKey
field tolist_editable
without a very good reason and a very small related object set. - 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.
- 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 Caz
s, 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.
Recommendations
- Avoid including
ForeignKey
fields in__str__
or__unicode__
methods for other Models. - Use
__repr__
instead for dumping helpful information out when prowling around in the shell. - Define a custom property for outputting full object information in templates where the query count is manageable.
- 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.
- Use raw SQL code to generate the information you need. The above for a list view could easily be replaced by some
JOIN
s in a query.
Comment on reddit’s /r/django.
-
Hat tip to spookylukey. ↩︎