Django 2でモデルにMeta.orderingを指定すると.values().annotate()が不正な結果を返す問題

はじめに

Django(2.1.7)で集計処理( values().annotate() )が Meta.ordering を追加した途端に誤った結果を返すようになりました。

問題

例として以下の Foo モデルを用意します。 ここで created_at フィールドを Meta.ordering に指定しておきます。

class Foo(models.Model):
    foo = models.CharField(max_length=255) #  GROUP BYに指定するフィールド
    bar = models.IntegerField() #  SUM()に指定するフィールド
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['created_at', ]

まず Foo モデルに集計対象となるデータを追加します。

❯ python manage.py shell
>>> from example.models import Foo
>>> Foo.objects.create(foo="1", bar=10)
>>> Foo.objects.create(foo="1", bar=10)
>>> Foo.objects.create(foo="2", bar=10)

続けて、 foo フィールドをGROUP BY項目として bar フィールドを集計します。
しかし、結果として返される QuerySet は集計されていません。

>>> from django.db.models import Sum
>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar'))
<QuerySet [{'foo': '1', 'sum': 10}, {'foo': '1', 'sum': 10}, {'foo': '2', 'sum': 10}]>

原因

この事象については公式ドキュメントに記載があります。

docs.djangoproject.com

Deprecated since version 2.2: Starting in Django 3.1, the ordering from a model’s Meta.ordering won’t be used in GROUP BY queries, such as .annotate().values(). Since Django 2.2, these queries issue a deprecation warning indicating to add an explicit order_by() to the queryset to silence the warning.

Django 2では Meta.orderingvalues().annotate() の結果に影響するため明示的に order_by() を指定する必要があります。 なお、Django 2.2からは警告が出力され、Django 3.1(2020/8リリース予定)では Meta.ordering が無視されるように修正されるとのことです。

対策

order_by() を追加し、期待通り集計できることを確認します。

>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar')).order_by('foo')
<QuerySet [{'foo': '1', 'sum': 20}, {'foo': '2', 'sum': 10}]>
>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar')).order_by('bar')
<QuerySet [{'foo': '1', 'sum': 20}, {'foo': '2', 'sum': 10}]>
>>> Foo.objects.all().values('foo').annotate(sum=Sum('bar')).order_by('created_at') #  クエリに含まないフィールドの指定では不正な結果を返す
<QuerySet [{'foo': '1', 'sum': 10}, {'foo': '1', 'sum': 10}, {'foo': '2', 'sum': 10}]>

なお、 Meta.ordering で例えば foo フィールドを指定していた場合には問題なく集計できます。 values().annotate() で構成するクエリに含まないフィールドで order_by が適用されてしまうことが問題のようです。

まとめ

Django 2を使用していて集計処理がうまくいかない場合は Meta.ordering を疑ってみると良いかもしれません。