Djangoの.all()
や.filter()
で値を取得した結果得られるオブジェクトは、DjangoのQueryset型になっています。
.order_by()
一発で並べ替えることが出来る単純なソートなら良いですが、値に応じて順番を変えるなど少し凝ったことをする場合はfor文で回す必要があり、コードもやや煩雑になるので、できればレコード取得と同時に.order_by()
で並べ替えを完了させておきたい所です。そんな時に、.annotate()
を用いることでより高度な並べ替えを行う方法を紹介したいと思います。
※ querysetの基本的な扱いを理解していることを前提とした記事になっているので、.all()
や.filter()
、.order_by()
などが分からない方は先に下記の記事などを参照していただければ幸いです。
参考:
Django逆引きチートシート(QuerySet編)
annotateとは
「annotate」とは、「注釈をつける」という意味を持った英単語です。使用方法としては、他のクエリメソッド同様、レコード取得の後に.
で連結する形になります。
Book.objects.all().annotate(new_field="value")
これを使用すれば、**「モデルが持っているフィールドに加えて、こちらで指定したフィールドを追加で出力する」**ということができるようになります。
ちなみに、docstringには下記のような説明が書かれています。
Return a query set in which the returned objects have been annotated with extra data or aggregations.
(返されたオブジェクトが、追加データや集約でアノテーションされているクエリセットを返す)
参照: https://github.com/django/django/blob/master/django/db/models/query.py
テーブル例
説明のため、下記のようなテーブルがあるとします。
テーブル | 説明 |
---|---|
Book | 本の情報を保持する。 |
AwardBook | 受賞歴のある本のIDを保持する。 |
これは、Djangoのモデルでは下記のように書けます。
from django.db import models
class Book(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=50) # 本の題名
price = models.IntegerField() # 本の値段
class AwardBook(models.Model):
id = models.AutoField(primary_key=True)
book = models.ForeignKey(Book, on_delete=models.CASCADE) # 受賞歴のある本のID
※「Bookテーブルに受賞歴フラグを持たせれば良い」という話ではありますが、今回はあくまで簡略化した例なので、諸事情でテーブルを分ける必要があることとします。
使い方の例
例えば、本の一覧を取得した後、**値段が安い順(=昇順)**に並べ替えたい場合、下記のように書けます。
queryset = Book.objects.all().order_by("price")
ちなみに、**値段が高い順(=降順)**にしたい場合、フィールド名の最初に-
を付けます。
queryset = Book.objects.all().order_by("-price")
ただし、複数の要素で並べ替えをしなくてはならず、その条件が下記のような場合はどうすれば良いでしょうか。
優先①: 受賞歴がある
優先②: 値段が安い
filter
を使えば、「受賞歴のある本のみを取得する」ことは出来ますが、それ以外の本を同時に取得することが出来ません。そんな時、annotate
を使えば次のように書くことが出来ます。
from django.db.models import Case, When, Value, IntegerField
from .models import Book, AwardBook
# 受賞歴のあるの本のIDを取得する
award_book_ids = set( # 計算量を減らすため、setに変換
AwardBook.objects.all().values_list(
"book_id", flat=True
)
)
# 並び替えを行いつつ、本一覧を取得
queryset = (
Book.objects.all()
.annotate( # annotateで新しいフィールドを付与
# 「受賞歴があるかどうか」を表すフラグ
award_flg=Case(
# 本のIDが「award_book_ids」に含まれている場合、値を1に設定
When(id__in=award_book_ids, then=Value(1)),
# それ以外の場合、値を0に設定
default=Value(0),
# それらの値のフィールドは「IntegerField」にする
output_field=IntegerField(),
)
)
.order_by(
"award_flg", # 1. 受賞歴のある本一覧の中に存在するか
"price", # 2. 値段が安い順
)
)
annotate
を用いて、award_flg
という「受賞歴があるかどうか」を表すフラグを新たに設け、そちらのフラグが1になっているものを優先するロジックになっています。
ここで、新たに登場したCase、Whenについて説明したいと思います。
Case, When
Case
およびWhen
は、SQL文のCASE式を表現するために使用することのできるクラスです。
CASE式を使用すれば、「あるカラムの値に応じ、別の値を割り当てる」という処理を行うことができます。例えば、「テストの点数に応じて、成績をS,A,B,C,Fで割り振りたい」という処理を行う場合、下記のように書くことができます。
CASE
WHEN score >= 90 THEN 'S'
WHEN score >= 80 THEN 'A'
WHEN score >= 70 THEN 'B'
WHEN score >= 60 THEN 'C'
ELSE 'F'
END
なお、式は上から順に処理されるので、2番目の式は自動的に80 <= score < 90
という意味になります。
上記の成績評価をDjangoのCase
, When
で表現すると、下記のように書くことができます。
from django.db.models import Case, When, Value, CharField
grade = Case(
When(score__gte=90, then=Value("S")),
When(score__gte=80, then=Value("A")),
When(score__gte=70, then=Value("B")),
When(score__gte=60, then=Value("C")),
default=Value("F"),
output_field=CharField(max_length=1)
)
CASE式のELSEに該当するパラメータがdefault
になっていることに注意してください。また、出力用のフィールドを指定することが必須であり、今回は文字列なのでCharField
を指定しています。
{field}__gte
の他にも、{field}__in
や{field}__contains
など、querysetで用いることのできる演算子は使用することができます。もちろん、そのままイコールの意味として使いたい場合、下記のように=
だけでつなげば大丈夫です。
When(score=100, then=Value("SS"))
まとめ
-
annotate
による、新しいフィールドの追加 -
Case
・When
による、条件に応じた値の割り当て -
order_by
で新しく追加されたフィールドを指定し、並べ替え
これらを組み合わせることで、高度な並べ替えを実現することが出来ました。工夫次第でさらに多様なケースに対応することが出来るので、ぜひ活用していただければ幸いです。