19
14

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.

iRidgeAdvent Calendar 2020

Day 12

【Django】annotateを使って高度なorder_byを書く方法

Last updated at Posted at 2020-12-12

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

参考: CASE式で条件分岐をSQL文に任せる

まとめ

  • annotateによる、新しいフィールドの追加
  • CaseWhenによる、条件に応じた値の割り当て
  • order_byで新しく追加されたフィールドを指定し、並べ替え

これらを組み合わせることで、高度な並べ替えを実現することが出来ました。工夫次第でさらに多様なケースに対応することが出来るので、ぜひ活用していただければ幸いです。

19
14
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
19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?