I'm experiencing some major performing issue with my django admin. Lots of duplicate queries based on how many inlines that I have.
models.py
class Setting(models.Model):
name = models.CharField(max_length=50, unique=True)
class Meta:
ordering = ('name',)
def __str__(self):
return self.name
class DisplayedGroup(models.Model):
name = models.CharField(max_length=30, unique=True)
position = models.PositiveSmallIntegerField(default=100)
class Meta:
ordering = ('priority',)
def __str__(self):
return self.name
class Machine(models.Model):
name = models.CharField(max_length=20, unique=True)
settings = models.ManyToManyField(
Setting, through='Arrangement', blank=True
)
class Meta:
ordering = ('name',)
def __str__(self):
return self.name
class Arrangement(models.Model):
machine = models.ForeignKey(Machine, on_delete=models.CASCADE)
setting = models.ForeignKey(Setting, on_delete=models.CASCADE)
displayed_group = models.ForeignKey(
DisplayedGroup, on_delete=models.PROTECT,
default=1)
priority = models.PositiveSmallIntegerField(
default=100,
help_text='Smallest number will be displayed first'
)
class Meta:
ordering = ('priority',)
unique_together = (("machine", "setting"),)
admin.py
class ArrangementInline(admin.TabularInline):
model = Arrangement
extra = 1
class MachineAdmin(admin.ModelAdmin):
inlines = (ArrangementInline,)
If I have 3 settings added on inline form and 1 extra, I have about 10 duplicate queries
SELECT "corps_setting"."id", "corps_setting"."name", "corps_setting"."user_id", "corps_setting"."tagged", "corps_setting"."created", "corps_setting"."modified" FROM "corps_setting" ORDER BY "corps_setting"."name" ASC
- Duplicated 5 times
SELECT "corps_displayedgroup"."id", "corps_displayedgroup"."name", "corps_displayedgroup"."color", "corps_displayedgroup"."priority", "corps_displayedgroup"."created", "corps_displayedgroup"."modified" FROM "corps_displayedgroup" ORDER BY "corps_displayedgroup"."priority" ASC
- Duplicated 5 times.
Could someone please tell me what I'm doing wrong right here? I've spent 3 days trying to figure the problem out myself without luck.
The issue gets worse when I have about 50 settings inlines of a Machine, I will have ~100 queries.
This is pretty much normal behaviour in Django - it doesn't do the optimization for you, but it gives you decent tools to do it yourself. And don't sweat it, 100 queries isn't really a big problem (I've seen 16k queries on one page) that needs fixing right away. But if your amounts of data are gonna increase rapidly, then it's wise to deal with it of course.
The main weapons you'll be armed with are queryset methods
select_related()
andprefetch_related()
. There's really no point of going too deeply into them since they're very well documented here, but just a general pointer:use
select_related()
when the object you're querying has only one related object (FK or one2one)use
prefetch_related()
when the object you're querying has multiple related objects (the other end of FK or M2M)And how to use them in Django admin, you ask? Elementary, my dear Watson. Override the admin page method
get_queryset(self, request)
so it would look sth like this:EDIT: Having read your comment, I realise that my initial interpretation of your question was absolutely wrong. I do have multiple solutions for your problem as well and here goes that:
The simple one that I use most of the time and recommend: just replace the Django default select widgets with
raw_id_field
widgets and no queries are made. Just setraw_id_fields = ('setting', 'displayed_group')
in the inline admin and be done for.But, if you don't want to get rid of the select boxes, I can give some half-hacky code that does the trick, but is rather lengthy and not very pretty. The idea is to override the formset that creates the forms and specify choices for these fields in the formset so that they're only queried once from the database.
Here it goes:
If you find something that could be improved or have any questions, let me know.
I've assembled a generic solution based on @makaveli's answer that doesn't seem to have problem mentioned in the comments:
All you'll need to do is subclass your model from CachingModelChoicesForm and use CachingModelChoicesFormSet in your inline class: