Django забирает только последний объект связанной модели

Рассмотрите следующие модели:

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)

И следующий код:

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

Вышеуказанное приводит к N+1 запросу, как и ожидалось. Я попытался исправить это, выполнив предварительную выборку books следующим образом:

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

Однако это не решает проблему N+1. Я думаю, причина в том, что предварительная выборка выполняет запрос SELECT * FROM book WHERE author_id IN (1,2,...), который отличается от запроса, выполняемого вызовом .latest(), т.е. SELECT * FROM book WHERE author_id = 1 ORDER BY created_at DESC LIMIT 1. Предварительная выборка делает IN, а .latest() делает =.

Я также безуспешно пробовал следующее:

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

Как должна выглядеть предварительная выборка, чтобы избежать N+1 селектов при использовании .latest()?

Вы можете сделать это с помощью некоторого пользовательского кода:

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

Объекты Author в authors, будут иметь дополнительный атрибут last_book, если существует хотя бы одна книга для этого автора с последней книгой.

Логика здесь в некоторой степени соответствует тому, что Django .prefetch_related делает за занавесками: вместо этого он должен был бы получить все книги, связанные с этими авторами, а затем создать для каждого объекта объект, в котором он обернет связанные Book объекты в коллекцию.

Вернуться на верх