Home > Net >  Django prefetch only latest object of related model
Django prefetch only latest object of related model

Time:10-24

Consider the following models:

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author, related_name="books", on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)

And the following code:

queryset = Author.objects.all()
for author in queryset:
    print(author.name)
    print(author.books.latest("created_at").title)

The above causes an N 1 query as expected. I tried fixing it by prefetching the books like so:

queryset = Author.objects.prefetch_related('books')

However, this does not resolve the N 1 issue. I think the reason is the prefetch does a SELECT * FROM book WHERE author_id IN (1,2,...) which is different from the query performed by the call to .latest(), i.e. SELECT * FROM book WHERE author_id = 1 ORDER BY created_at DESC LIMIT 1. The prefetch does an IN and .latest() does an =.

I have also tried the following without success:

queryset = Author.objects.prefetch_related(Prefetch('books', queryset=Book.objects.order_by("-created_at")))

What should the prefetch look like in order to avoid N 1 selects when using .latest()?

CodePudding user response:

You can do this with some custom code:

from django.db.models import OuterRef, Subquery


authors = Author.objects.annotate(
    last_book_id=Subquery(
        Book.objects.filter(author_id=OuterRef('pk')).order_by('-created_at').values('pk')[:1]
    )
)

author_dict = {author.pk: author for author in authors}

last_books = Book.objects.filter(
    pk__in=[author.last_book_id for author in authors if author.last_book_id is not None]
)

for book in last_books:
    author_dict[book.author_id].last_book = book

The Author objects in the authors, will have an extra attribute last_book, if there exists at least one book for that author with the last book.

The logic here is to some extent what Django's .prefetch_related does behind the curtains: instead it would have fetched all books related to these authors, and then create objects an object for each object where it will wrap the related Book objects into a collection.

  • Related