DEV Community

Cover image for Why ForeignKey reference in Django model's __str__ method is a bad idea
Constantine
Constantine

Posted on Edited on

Why ForeignKey reference in Django model's __str__ method is a bad idea

Cutting database queries by not referencing Foreign Keys. Or fixing bad behavior.

Photo by israel palacio on Unsplash

So you have a model with foreign key:

from django.db import models
from .models import Author

class Book(models.Model):
    name = models.CharField(...)
    author = models.ForeignKey(to=Author,...)
Enter fullscreen mode Exit fullscreen mode

Now you want to see an author in your admin panel. Easy, right? Just add:

def __str__(self):
    return f'{self.id} {self.author.name}'
Enter fullscreen mode Exit fullscreen mode

What this will do is hit our database for an author name for every row present on a page. Imagine there are 100 rows. That's 100 more hits. This problem is known as the n+1 query.

The solution is quite simple. If that's a ForeignKey, you need to use select_related on your queryset. If that's a ManyToMany field or reverse FK, then you need a prefetch_related method. And, by the way, that's the difference between those two methods. It's a common interview question.

But in our case it's not a simple query. If you really want to fix that you would need to dig deeper into how Django admin works and provide custom queryset to the view.

When I encountered this problem the solution that has been accepted was just removing the reference to ForeingKey from the __str__ method ¯\_(ツ)_/¯

A little ToDo for myself: find a way to solve this via select_related.

UPDATE after half a year

For some time I didn't face this problem but not long ago I did. So, as I said, the solution to this is quite simple. All we need to do is to redefine get_queryset method on admin model. For example:

@admin.register(models.Book)
class BookAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = self.model._default_manager.select_related(
            'author',
        )
        # This stuff is marked as TODO in Django 3.1
        # so it might change in the future
        ordering = self.get_ordering(request)
        if ordering:
            qs = qs.order_by(*ordering)

        return qs
Enter fullscreen mode Exit fullscreen mode

Now there will be just one query per record, containing JOIN clause, hitting our database.

Optimization FTW 😏

Top comments (5)

Collapse
 
c_v_ya profile image
Constantine •

Sure, but you can access that inside the model page (e.g. /admin/model/id). Or generate a report. But throwing hundreds of queries when loading one page is really bad. I still had no time to figure out how to prefetch everything in admin view but there should be a way.

Collapse
 
vergeev profile image
Pavel Vergeev •

You can denormalize the tables, duplicating author name on the Book instances. But only in case you actually need the data and cannot afford the overhead, of course.

Collapse
 
c_v_ya profile image
Constantine •

Fair point. For some reason I started to come across on a lot of DB denormalization advises quite often lately. And wondering if that's a trend now?

Collapse
 
steelwolf180 profile image
Max Ong Zong Bao • • Edited

Hmmm... why would you have many to many field? I smell like bad database design if there is a many to many field in the database table.

Collapse
 
c_v_ya profile image
Constantine •

lolwhat