web-dev-qa-db-ja.com

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()))

最後の2行は、各Postオブジェクトのタグの数を示しています。そして、ここでも同じエラーが発生し続けます:

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

この例の問題の1つは、queryset.count()がクエリセットを評価してカウントを返そうとするため、.count()をサブクエリとして使用できないことです。

そのため、代わりにCount()を使用するのが正しいアプローチだと思うかもしれません。たぶんこのようなもの:

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

これは2つの理由で機能しません。

  1. TagクエリセットはすべてのTagフィールドを選択しますが、Countは1つのフィールドのみをカウントできます。したがって:Tag.objects.filter(post=OuterRef('pk')).only('pk')が必要です(_tag.pk_でカウントを選択するには)。

  2. Count自体はSubqueryクラスではなく、CountAggregateです。したがって、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_句があることに気づくかもしれません。これは、Countが集計であるため、現時点では結果に影響しませんが、他の場合には影響する可能性があるためです。 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"
_
49
Todor

Django-sql-utils パッケージは、この種のサブクエリ集約を簡単にします。ただpip install Django-sql-utils その後:

from sql_util.utils import SubqueryCount
posts = Post.objects.annotate(tag_count=SubqueryCount('tag'))

SubqueryCountのAPIはCountと同じですが、関連テーブルに結合する代わりにSQLで副選択を生成します。

0