1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Django】逆参照してCountとる時に条件(filter)を指定したい時

Posted at

めちゃくちゃググったんですけどなかなか答えにたどり着けなかったので残しておきます。

やりたきこと

そのタグが設定されている公開済みの記事の数をリストでほしかったんです。

ほしかった結果

[
    {
        "id": 2,
        "name": "ウマ娘 プリティーダービー",
        "post_count": 3
    },
    {
        "id": 6,
        "name": "オーバーウォッチ",
        "post_count": 1
    },
    {
        "id": 3,
        "name": "NIKKE",
        "post_count": 1
    },
    {
        "id": 8,
        "name": "ビートマニア",
        "post_count": 0
    },

ここからは実装の内容

記事モデル(post)と、タグモデル(tag)がありまして、記事モデルのほうからManytoManyでリレーション貼ってる感じです。


class Tag(models.Model):
    name = models.CharField(max_length=40)


class Post(models.Model):
    class Statuses(models.IntegerChoices):
        PUBLISHED = 1
        DRAFT = 2
        PRIVATE = 3
        DELETED = 4

    user = models.ForeignKey(Profile, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    content = models.TextField()
    status = models.IntegerField(choices=Statuses.choices)
    tags = models.ManyToManyField(Tag, blank=False)

こんな感じです。

で、最初はこう書きました。

Tag.objects.filter(post__status=1).annotate(post_count=Count('post'))

一瞬いけたと思ったんですよ。正直にこれにたどり着くまでにも時間かかってたので勝った気でいました。
が、だめでした。
テストコード走らせて気づいたんですけどこれだと、タグがあっても、記事が存在しないと、タグ自体が返ってこないんですよね。

annotateする前に、filterで絞っちゃってるから「まあそうか」って感じなんですけど。
テストコードに救われましたよね。みんなテストコード書きましょう。

じゃあどうしたか

from django.db.models import Q
Tag.objects.annotate(post_count=Count('post', filter=Q(post__status=1)))

Countクラスにfilterを追加することでいけました。

以下蛇足

これなかなか答えにたどり着けなかったんですよね。
https://y0m0r.hateblo.jp/entry/20120601/1338552629
こちらの記事でも同じことやろうとして、同じ問題にぶつかってるんですけどこちらはextraを使ってなんとかしたようですが、僕はどうしても諦められなかったんですよね。(こちらの記事は2012年に書かれたものなので当時はこれしか方法がなかったのかもしれません)

で、最終的にどうしたかというと、Countのクラスの中身を見てみたんです。

django.db.models.aggregates.py
class Count(Aggregate):
    function = "COUNT"
    name = "Count"
    output_field = IntegerField()
    allow_distinct = True
    empty_result_set_value = 0

    def __init__(self, expression, filter=None, **extra):
        if expression == "*":
            expression = Star()
        if isinstance(expression, Star) and filter is not None:
            raise ValueError("Star cannot be used with filter. Please specify a field.")
        super().__init__(expression, filter=filter, **extra)

あるじゃん!! filter !!
つまりできるはずなんです。
で、色々いじってみたら、上記にも書いてますけど、

from django.db.models import Q
Tag.objects.annotate(post_count=Count('post', filter=Q(post__status=1)))

でいけましたとさ。

ドキュメントはこちら
https://docs.djangoproject.com/en/4.1/topics/db/aggregation/

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?