4

I have the following Django models:

from django.db import models from django.utils.translation import gettext_lazy as _ class Enterprise(models.Model): nome = models.CharField(max_length=255) class MediaTopic(models.Model): name = models.CharField(max_length=255) class EnterpriseRelatedMedia(models.Model): enterprise = models.ForeignKey( Enterprise, on_delete=models.SET_NULL, null=True, blank=True, related_name="related_medias", related_query_name="related_media", ) topic = models.ForeignKey( MediaTopic, on_delete=models.DO_NOTHING, related_name="enterprise_related_medias", related_query_name="enterprise_related_media", blank=True, null=True, ) 

And this is my admin.py:

from django.contrib import admin from . import models class EnterpriseRelatedMediaInline(admin.StackedInline): model = models.EnterpriseRelatedMedia extra = 1 @admin.register(models.Enterprise) class EnterpriseAdmin(admin.ModelAdmin): inlines = [ EnterpriseRelatedMediaInline, ] admin.site.register(models.MediaTopic) 

The problem is that since my inline contains not only the enterprise foreignkey but also an extra foreignkey pointing to MediaTopic, everytime my django admin change page loads, it makes a lot of queries to the database. Im aware that this is the famous N+1 problem, and that a select_related or prefetch_related would solve it, the problem is that i do not know where to put it, i tried putting it in the EnterpriseAdmin class as well as the EnterpriseRelatedMediaInline class, and it doesn't seem to work! Every time the admin loads the page, for each instance of the inline (which are 10), it makes a query to pull all the instances of MediaTopic to be available in the foreignkey attribute of the form.

Im using Django Debug Toolbar, and this is how the change page looks like:

(Notice how it does 17 queries on the bottom right when it says "SQL")

My Django Admin Change Page

these are the queries being performed:

The queries being made

Again, im aware that its because its pulling all instances of MediaTopic for each instance of EnterpriseRelatedMedia shown as an Inline, but i do not know where to put the select_related and prefetch_related! Please help

And yes, i made the test, and when i remove the "topic" attribute from EnterpriseRelatedMedia model, the problem stops, and it makes only a few queries, 5 at maximum

5
  • This is because it populates the dropdowns, the FormSets have often some performance issues: a dropdown could be fetched once, and then run, but unfortunately it is essentially not much more than a collection of individual forms doing the same work multiple times. Commented Nov 4, 2024 at 8:36
  • isn't there any way to customize this behaviour? something like putting the queryset in the EnterpriseAdmin class only for it to be used (without requerying) in the formsets? Commented Nov 4, 2024 at 8:59
  • well I did built such thing once, by essentially using a proxy-QuerySet, that prevented .all() from "cloning" the QuerySet, and thus using the cache of the QuerySet, but it turns out is not that easy: when you save the form, it then performs filters on these QuerySets, so it is not that easy to patch it. Commented Nov 4, 2024 at 9:02
  • Perhaps the easiest way would be to use an autocomplete_fields approach, where you turn these into select2s, that thus lazily query the thing. Commented Nov 4, 2024 at 9:08
  • makes sense, i'll give it a try, thanks! Commented Nov 4, 2024 at 9:11

1 Answer 1

2

This is one of the consequences of the FormSet: this is a collection of Forms, and each Form is populated with a dropdown by an individual QuerySet, so for each item in the inline that has such dropdown, it will make a query, and if the number of items in the inline is large, a lot of queries will be used.

Perhaps the most convenient way to get rid of this is to add topic to the .autocomplete_fields [Django-doc], then it will lazily populate these with AJAX calls, and combine this with a .select_related('topic') for the initial population of the FormSet:

class EnterpriseRelatedMediaInline(admin.StackedInline): model = models.EnterpriseRelatedMedia autocompete_fields = ('topic',) extra = 1 def get_queryset(self, *args, **kwargs): return super().get_queryset(*args, **kwargs).select_related('topic')

This thus means that you add a search_fields = ['name'] on the ModelAdmin of the MediaTopic model.

You can probably also prefetch the elements with the corresponding topics by using a Prefetch object [Django-doc]:

from django.db.models import Prefetch @admin.register(models.Enterprise) class EnterpriseAdmin(admin.ModelAdmin): inlines = [ EnterpriseRelatedMediaInline, ] def get_queryset(self, *args, **kwargs): return ( super() .get_queryset(*args, **kwargs) .prefetch_related( Prefetch( 'related_medias', EnterpriseRelatedMedia.objects.select_related('topic'), ) ) )
Sign up to request clarification or add additional context in comments.

3 Comments

It worked for the Topic dropdown in each inline instance of EnterpriseRelatedMedia, but the admin is still performing a query for each instance of EnterpriseRelatedMedia, which means that for my case when i have 50 instances on the real project, it still makes 50 queries. I think i need to find a way to Lazy Load the inlines, but either way, that was really helpful, thank you very much!
@IvesFurtado: what if you prefetch in the EnterpriseAdmin?
Unfortunately, it doesn't seem to help using the Prefetch object, it still performas a query for each instance of the inline, it appears to ignore completely the queryset from the EnterpriseAdmin class and build the inlines from scratch always, thus ignoring any select_related or prefetch_related (doesn't matter if its made in EnterpriseAdmin or EnterpriseRelatedMediaAdmin)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.