Djangoのannotateの今風な使い方: リレーション数、最大値、最小値、平均値の集計

301, 2021-08-06

目次

Djangoのannotateの今風な使い方

Djangoではモデルの集計を行うための関数がいくつか用意されています。
その中の1つが今回紹介するannotate()関数です。

annotate()を使うとForeignKeyなどでリレーションしているモデルの集計を簡単に行うことが出来ます。
たとえばAというモデルを持っているBのフィールドCAについて集計する、といった具合です。

この記事ではannotate()について具体的に↓を見ていきます。

  • 解説の前提とするモデル

  • 解説の前提とするクラス

  • データベースの初期化

  • annotate()の構造

  • 評価数の集計

  • 最大評価の集計

  • 最小評価の集計

  • 平均評価の集計

関連記事
Djangoのexcludeで指定のレコードを除外する
DjangoのQuerySetのdistinct()の使い方

解説の前提とするモデル

今回のannotate()の解説で使用するモデルは↓のような定義になります。

from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator


class Response(models.Model):
    """
    ブログへのレスポンス
    """
    name = models.CharField(max_length=64, help_text='投稿者名')
    content = models.TextField(max_length=512, help_text='投稿内容')


class Evaluation(models.Model):
    """
    レスポンスへの評価
    星の数(stars)で評価する
    """
    STARS_CHOICES = (
        ('☆☆☆☆☆', 5),
        ('☆☆☆☆★', 4),
        ('☆☆☆★★', 3),
        ('☆☆★★★', 2),
        ('☆★★★★', 1),
    )

    stars = models.IntegerField(
        choices=STARS_CHOICES,
        validators=[MinValueValidator(1), MaxValueValidator(5)],
        help_text='星',
    )
    response = models.ForeignKey(Response, on_delete=models.PROTECT, help_text='評価先のレス')

ブログを構成するblogというアプリを作ってあります。
そしてそのモデルが↑です。
Responseはブログの記事へのレスポンスを表し、Evaluationはそのレスポンスの評価を表します。
Evaluationはよくある星の5段階評価になってます。

解説の前提とするクラス

以降のコードでは省略しますが、↓のクラスを事前にインポートしている前提で解説を行います。
あらかじめご了承ください。

from django.db.models import Count, Max, Min, Avg
from blog.models import Response, Evaluation

データベースの初期化

データベースを↓のように初期化します。

response_1 = Response.objects.create(name='太郎', content='こんにちは')
response_2 = Response.objects.create(name='マイケル', content='こんにちは')
response_3 = Response.objects.create(name='スーパーボブ', content='Hi, Guys')

Evaluation.objects.create(response=response_1, stars=3)
Evaluation.objects.create(response=response_1, stars=4)
Evaluation.objects.create(response=response_1, stars=5)
Evaluation.objects.create(response=response_1, stars=1)
Evaluation.objects.create(response=response_2, stars=2)
Evaluation.objects.create(response=response_2, stars=2)
Evaluation.objects.create(response=response_2, stars=4)
Evaluation.objects.create(response=response_3, stars=5)

レスポンスを3つ作り、それぞれのレスポンスに評価を加えています。

annotate()の構造

annotate()QuerySetのメソッドです。
その構造は↓のようになっています。

def annotate(self, *args, **kwargs):
    ...

可変長引数と可変長キーワード引数を取るだけのシンプルなメソッドです。
内部では_annotate()に処理を委譲しています。

annotate()QuerySetのメソッドなので、↓のようにモデルのobjectsから使うことが出来ます。

Response.objects.annotate()

また、annotate()の結果もQuerySetなので↓のように他のメソッドを繋げることも可能です。

Response.objects.annotate(Count('evaluation')).filter(evaluation__count__gte=2)

評価数の集計

Responseに繋がっているEvaluationの数を集計したい場合は↓のようにします。

# 評価数の集計
for response in Response.objects.annotate(Count('evaluation')):
    print(f'投稿者名[{response.name}] 評価数[{response.evaluation__count}]')
投稿者名[太郎] 評価数[4]
投稿者名[マイケル] 評価数[3]
投稿者名[スーパーボブ] 評価数[1]

Responseannotate()Count()を渡すと、Count()の引数のモデル数(あるいはフィールド名)をカウントしてくれます。
↑の場合はCount('evaluation')としていますので、Responseに紐づけられているEvaluationの数がカウントされます。
カウント結果は「クラスの引数__クラス名」というフォーマットで、モデルに保存されます。↑の例で言うと、evaluation__countという属性がresponseに保存されています。
このevaluation__countが実際の集計の値になります。

↑の出力結果では投稿者名「太郎」の評価数が「4」になっていますが、これは太郎に紐づけられているEvaluationの数です。

最大評価の集計

レスポンスに繋がっている最大評価の集計を行うには↓のようにします。

# 最大評価の集計
for response in Response.objects.annotate(Max('evaluation__stars')):
    print(f'投稿者名[{response.name}] 最大評価[{response.evaluation__stars__max}]')
投稿者名[太郎] 最大評価[5]
投稿者名[マイケル] 最大評価[4]
投稿者名[スーパーボブ] 最大評価[5]

最大評価の集計にはMax()annotate()に渡します。
Max()の引数には「モデル名__フィールド名」というフォーマットで集計したいフィールドを指定します。
↑の例ではEvaluationstarsを指定しています。
↑の場合、「太郎」の最大評価は「5」で、「マイケル」の最大評価は「4」になっています。

最小評価の集計

レスポンスに繋がっている最小評価の集計を行うには↓のようにします。

# 最小評価の集計
for response in Response.objects.annotate(Min('evaluation__stars')):
    print(f'投稿者名[{response.name}] 最小評価[{response.evaluation__stars__min}]')
投稿者名[太郎] 最小評価[1]
投稿者名[マイケル] 最小評価[2]
投稿者名[スーパーボブ] 最小評価[5]

最小評価の集計を行うにはMin()annotate()に渡します。
↑の場合、「太郎」の最小評価は「1」で、マイケルの最小評価は「2」になっています。

平均評価の集計

レスポンスに繋がっている評価の平均値を集計するには↓のようにします。

# 平均評価の集計
for response in Response.objects.annotate(Avg('evaluation__stars')):
    print(f'投稿者名[{response.name}] 最小評価[{response.evaluation__stars__avg}]')
投稿者名[太郎] 最小評価[3.25]
投稿者名[マイケル] 最小評価[2.6666666666666665]
投稿者名[スーパーボブ] 最小評価[5.0]

平均値を集計するにはAvg()annotate()に渡します。
↑の場合、太郎の評価数は4つで評価はそれぞれ3, 4, 5, 1ですが、その平均値3.25が集計できています。

(^ _ ^)

( 3 + 4 + 5 + 1 ) / 4 = 3.25 ですね

複数の集計

たとえば最大値と最小値の評価を同時に集計したい場合があります。
そういう場合はannotate()に複数のクラス(Max, Minなど)を渡せば集計可能です。

# 最大評価と最小評価の集計
for response in Response.objects.annotate(Min('evaluation__stars'), Max('evaluation__stars')):
    print(f'投稿者名[{response.name}] 最大評価[{response.evaluation__stars__max}] 最小評価[{response.evaluation__stars__min}]')
投稿者名[太郎] 最大評価[5] 最小評価[1]
投稿者名[マイケル] 最大評価[4] 最小評価[2]
投稿者名[スーパーボブ] 最大評価[5] 最小評価[5]

集計変数のエイリアス

集計変数のエイリアスを作ることができます。
annotate()にキーワード引数でクラスを指定すると、そのクラスの結果がそのキーワード引数名のフィールドに保存されます。

# 集計変数のエイリアス
for response in Response.objects.annotate(max_stars=Max('evaluation__stars')):
    print(f'投稿者名[{response.name}] 最大評価[{response.max_stars}]')

↑の場合、max_starsというのがエイリアスです。
max_starsResponseに保存され、それを参照することで↑の場合、最大評価を取得しています。

おわりに

今回はDjangoのannotate()を見てみました。
annotate()を使えば手軽に集計を行うことが可能です。

(^ _ ^)

評価数で勝負だ!

(・ v ・)

いいや最大評価で勝負だ!



この記事のアンケートを送信する