Простой подзапрос с OuterRef

Я пытаюсь сделать очень простой подзапрос, который использует OuterRef (не для практических целей, просто чтобы заставить его работать), но продолжает работать с той же ошибкой.

posts/models.py

from django.db import models

class Tag(models.Model):
    name = models.CharField(max_length=120)
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=120)
    tags = models.ManyToManyField(Tag)
    def __str__(self):
        return self.title

manage.py код оболочки

>>> from django.db.models import OuterRef, Subquery
>>> from posts.models import Tag, Post
>>> tag1 = Tag.objects.create(name='tag1')
>>> post1 = Post.objects.create(title='post1')
>>> post1.tags.add(tag1)
>>> Tag.objects.filter(post=post1.pk)
<QuerySet [<Tag: tag1>]>
>>> tags_list = Tag.objects.filter(post=OuterRef('pk'))
>>> Post.objects.annotate(count=Subquery(tags_list.count()))

последние две строки должны дать мне количество тегов для каждого объекта Post. И здесь я продолжаю получать ту же ошибку:

ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.

1 ответов


одна из проблем с вашим примером заключается в том, что вы не можете использовать queryset.count() как подзапрос, потому что .count() пытается оценить queryset и вернуть счетчик.

так можно подумать, что правильным подходом было бы использовать Count() вместо. Может быть, что-то вроде этого:

Post.objects.annotate(
    count=Count(Tag.objects.filter(post=OuterRef('pk')))
)

это не будет работать по двум причинам:

  1. на Tag queryset выбирает все Tag поля, в то время как Count можете рассчитывать только на одно поле. Таким образом: Tag.objects.filter(post=OuterRef('pk')).only('pk') необходимо (выбрать в расчете на tag.pk).

  2. Count сам по себе не является Subquery класса, Count это Aggregate. Таким образом, выражение, генерируемое Count не признан Subquery, мы можем исправить это с помощью Subquery.

применение исправлений для 1) и 2) приведет к:

Post.objects.annotate(
    count=Count(Subquery(Tag.objects.filter(post=OuterRef('pk')).only('pk')))
)

если вы проверяете создаваемый запрос

SELECT 
    "tests_post"."id",
    "tests_post"."title",
    COUNT((SELECT U0."id" 
            FROM "tests_tag" U0 
            INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id") 
            WHERE U1."post_id" = ("tests_post"."id"))
    ) AS "count" 
FROM "tests_post" 
GROUP BY 
    "tests_post"."id",
    "tests_post"."title"

вы можете заметить, что у нас есть GROUP BY предложения. Это потому, что граф-это совокупность, сейчас это не влияет на результат, но в некоторых других случаях. Вот почему docs предложите немного другой подход, где агрегация перемещается в subquery через определенную комбинацию values + annotate + values

Post.objects.annotate(
    count=Subquery(
        Tag.objects.filter(post=OuterRef('pk'))
            # The first .values call defines our GROUP BY clause
            # Its important to have a filtration on every field defined here
            # Otherwise you will have more than one group per row!!!
            # This will lead to subqueries to return more than one row!
            # But they are not allowed to do that!
            # In our example we group only by post
            # and we filter by post via OuterRef
            .values('post')
            # Here we say: count how many rows we have per group 
            .annotate(count=Count('pk'))
            # Here we say: return only the count
            .values('count')
    )
)

наконец-то это даст:

SELECT 
    "tests_post"."id",
    "tests_post"."title",
    (SELECT COUNT(U0."id") AS "count" 
            FROM "tests_tag" U0 
            INNER JOIN "tests_post_tags" U1 ON (U0."id" = U1."tag_id") 
            WHERE U1."post_id" = ("tests_post"."id") 
            GROUP BY U1."post_id"
    ) AS "count" 
FROM "tests_post"