Простой подзапрос с 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')))
)
это не будет работать по двум причинам:
на
Tag
queryset выбирает всеTag
поля, в то время какCount
можете рассчитывать только на одно поле. Таким образом:Tag.objects.filter(post=OuterRef('pk')).only('pk')
необходимо (выбрать в расчете наtag.pk
).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"