• en
  • Language: ru
  • Documentation version: 2.0

7. Как сделать подзапросное выражение в Django?

Django позволяет использовать подзапросы SQL. Давайте начнем с чего-то простого, У нас есть модель UserParent, которая имеет OnetoOne отношение с пользователем auth. Мы найдем все UserParent, у которых есть UserParent.

>>> from django.db.models import Subquery
>>> users = User.objects.all()
>>> UserParent.objects.filter(user_id__in=Subquery(users.values('id')))
<QuerySet [<UserParent: UserParent object (2)>, <UserParent: UserParent object (5)>, <UserParent: UserParent object (8)>]>

Теперь о чем-то более сложном. Для каждого Category мы хотим найти наиболее благосклонного Hero.

Модели выглядят примерно так.

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


class Hero(models.Model):
    # ...
    name = models.CharField(max_length=100)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)

    benevolence_factor = models.PositiveSmallIntegerField(
        help_text="How benevolent this hero is?",
        default=50
    )

Вы можете найти самого доброжелательного Героя следующим образом

hero_qs = Hero.objects.filter(
    category=OuterRef("pk")
).order_by("-benevolence_factor")
Category.objects.all().annotate(
    most_benevolent_hero=Subquery(
        hero_qs.values('name')[:1]
    )
)

Если вы посмотрите на сгенерированный sql, вы увидите

SELECT "entities_category"."id",
       "entities_category"."name",

  (SELECT U0."name"
   FROM "entities_hero" U0
   WHERE U0."category_id" = ("entities_category"."id")
   ORDER BY U0."benevolence_factor" DESC
   LIMIT 1) AS "most_benevolent_hero"
FROM "entities_category"

Давайте разберем логику кверисета. Первая часть

hero_qs = Hero.objects.filter(
    category=OuterRef("pk")
).order_by("-benevolence_factor")

Мы упорядочиваем объект Hero по benevolence_factor в порядке DESC и используем category=OuterRef("pk"), чтобы объявить, что мы будем использовать его в подзапросе.

Затем мы аннотируем most_benevolent_hero=Subquery(hero_qs.values('name')[:1]), чтобы получить использование подзапроса с кверисетом Category. Часть hero_qs.values('name')[:1] подхватывает первое имя из подзапроса.