Как использовать Subquery в Django ORM для аннотации моделей

Ответ

Subquery в Django ORM — это объект, который позволяет создавать вложенные SQL-запросы. Его основное преимущество — возможность выполнять сложные выборки и аннотации, которые зависят от другой таблицы, за один-единственный запрос к базе данных, избегая проблемы N+1.

Практический пример

Допустим, у нас есть модели Post и Comment, и мы хотим для каждого поста получить текст его последнего комментария.

1. Модели:

# models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

2. Запрос с Subquery:

# views.py
from django.db.models import OuterRef, Subquery
from .models import Post, Comment

# Создаем подзапрос, который для каждого поста (OuterRef('pk'))
# найдет самый новый комментарий.
latest_comment = Comment.objects.filter(
    post=OuterRef('pk')
).order_by('-created_at')

# Аннотируем каждый пост текстом последнего комментария.
# Подзапрос `latest_comment.values('text')[:1]` вернет
# только одно значение — текст одного комментария.
posts_with_last_comment = Post.objects.annotate(
    last_comment_text=Subquery(latest_comment.values('text')[:1])
)

# Теперь у каждого объекта `post` в `posts_with_last_comment`
# будет дополнительное поле `last_comment_text`.

Ключевые аспекты

  • OuterRef: Используется для ссылки на поле из внешнего запроса (в примере — pk модели Post).
  • Эффективность: Вся операция выполняется на уровне СУБД одним SQL-запросом, что гораздо производительнее, чем делать отдельные запросы для каждого объекта в цикле.
  • Ограничение результата: Подзапрос, используемый для аннотации или фильтрации, должен возвращать ровно одно значение (одну строку и один столбец). Для этого часто используется срез [:1].