Django1.8の新機能Conditional Expressions #djangoja

この記事は[TokyoDjangoMeetup #3](http://django.connpass.com/event/14219/)参加中に書きました。

## どんなことができるのか？
Django1.8で追加された新機能[Conditional Expressions](https://docs.djangoproject.com/en/1.8/ref/models/conditional-expressions/
)で、モデル操作時に使う`annotation`、`aggregation`、`update`メソッドに条件式を渡せるようになりました。

## サンプルのモデル
以下のモデルを用意しました。[^1]

``` django:models.py
# -*- coding: utf-8 -*-
from django.db import models


class Sponsor(models.Model):

    u"""
    PyCon JP 2015のスポンサー
    """
    DIAMOND = 'D'
    PLATINUM = 'P'
    GOLD = 'G'
    SILVER = 'S'
    PLAN_CHOICES = (
        (DIAMOND, 'Diamond'),
        (PLATINUM, 'Platinum'),
        (GOLD, 'Gold'),
        (SILVER, 'Silver'),
    )
    name = models.CharField(max_length=50)
    registered_on = models.DateField()
    plan = models.CharField(
        max_length=1,
        choices=PLAN_CHOICES,
        default=SILVER,
    )
```

テストデータは以下のとおりです。

```pycon
>>> from datetime import date, timedelta
>>> Sponsor.objects.create(
...     name='Crazy Inc.',
...     plan=Sponsor.DIAMOND,
...     registered_on=date.today() - timedelta(days=19))
<Sponsor: Sponsor object>
>>> Sponsor.objects.create(
...     name='Star Inc.',
...     plan=Sponsor.PLATINUM,
...     registered_on=date.today() - timedelta(days=36))
<Sponsor: Sponsor object>
>>> Sponsor.objects.create(
...     name='Experience Inc.',
...     plan=Sponsor.GOLD,
...     registered_on=date.today() - timedelta(days=11))
<Sponsor: Sponsor object>
>>> Sponsor.objects.create(
...     name='Giorno co., ltd.',
...     plan=Sponsor.GOLD,
...     registered_on=date.today() - timedelta(days=6))
<Sponsor: Sponsor object>
>>> Sponsor.objects.create(
...     name='Chariot Inc.',
...     plan=Sponsor.SILVER,
...     registered_on=date.today() - timedelta(days=1))
<Sponsor: Sponsor object>
>>> Sponsor.objects.create(
...     name='Polnareff co., ltd.',
...     plan=Sponsor.SILVER,
...     registered_on=date.today() - timedelta(days=2))
<Sponsor: Sponsor object>
```

## 実際に使ってみる
Conditional Expressionsは、以下の2つのクラスを組み合わせて書きます。

* [When](https://docs.djangoproject.com/en/1.8/ref/models/conditional-expressions/#when)クラス
    * 「条件」と「結果」をカプセル化したクラス
* [Case](https://docs.djangoproject.com/en/1.8/ref/models/conditional-expressions/#case)クラス
    * Pythonの`if ... elif ... else`のような条件式に当たる役割のクラス。「条件」が真となる`When`の「結果」を戻り値とする。

`When`クラスは、最初に「条件」、最後に「結果」を指定します。
「条件」の指定の仕方は[filter](https://docs.djangoproject.com/en/1.8/ref/models/querysets/#django.db.models.query.QuerySet.filter)メソッドと同じです。

```pycon
>>> from django.db.models import When, F, Q
>>> # planがGoldのときにnameフィールドを参照する
>>> When(plan=Sponsor.GOLD, then='name')
>>> When(plan=Sponsor.GOLD, then=F('name'))
>>> from datetime import date
>>> # registered_onが2015/2/1から2015/9/1の間ならplanフィールドを参照する
>>> When(registered_on__gt=date(2015, 2, 1),
...      registered_on__lt=date(2015, 9, 1),
...      then='plan')
>>> # QオブジェクトでOR条件を指定することもできる
>>> When(Q(name__startswith="Crazy") | Q(name__startswith="Giorno"),
...      then='name')
```

上記の`When`クラスを`Case`クラスと組み合わせて、各スポンサーのスポンサー料を求めてみましょう。

```pycon
>>> from django.db.models import Case, When, Value, CharField
>>> Sponsor.objects.annotate(
...     fee=Case(
...         When(plan=Sponsor.DIAMOND, then=Value(u'1,000,000円 (税抜き)')),
...         When(plan=Sponsor.PLATINUM, then=Value(u'500,000円 (税抜き)')),
...         When(plan=Sponsor.GOLD, then=Value(u'300,000円 (税抜き)')),
...         When(plan=Sponsor.SILVER, then=Value(u'100,000円 (税抜き)')),
...         output_field=CharField(),
...     ),
... ).values_list('name', 'fee')
[(u'Crazy Inc.', u'1,000,000\u5186 (\u7a0e\u629c\u304d)'), (u'Star Inc.', u'500,000\u5186 (\u7a0e\u629c\u304d)'), (u'Experience Inc.', u'300,000\u5186 (\u7a0e\u629c\u304d)'), (u'Giorno co., ltd.', u'300,000\u5186 (\u7a0e\u629c\u304d)'), (u'Chariot Inc.', u'100,000\u5186 (\u7a0e\u629c\u304d)'), (u'Polnareff co., ltd.', u'100,000\u5186 (\u7a0e\u629c\u304d)')]

```

`registered_on`の値に応じて`plan`を更新してみましょう。

```pycon
>>> from django.db.models import Case, When, Value
>>> from datetime import date,timedelta
>>> a_week_ago = date.today() - timedelta(weeks=1)
>>> a_month_ago = date.today() - timedelta(days=30)
>>> Sponsor.objects.update(
...     plan=Case(
...         When(registered_on__lte=a_week_ago ,
...              then=Value(Sponsor.PLATINUM)),
...         When(registered_on__lte=a_month_ago,
...              then=Value(Sponsor.GOLD)),
...         default=Value(Sponsor.SILVER)
...     ),
... )
>>> Sponsor.objects.values_list('name', 'plan')
[(u'Crazy Inc.', u'P'), (u'Star Inc.', u'P'), (u'Experience Inc.', u'P'), (u'Giorno co., ltd.', u'S'), (u'Chariot Inc.', u'S'), (u'Polnareff co., ltd.', u'S')]

```

各`plan`に何社応募したのか調べてみましょう。

```pycon
>>> from django.db.models import Case, When, Value, IntegerField, Sum
>>> Sponsor.objects.aggregate(
...     silver=Sum(
...         Case(When(plan=Sponsor.SILVER, then=1),
...              output_field=IntegerField())
...     ),
...     gold=Sum(
...         Case(When(plan=Sponsor.GOLD, then=1),
...              output_field=IntegerField())
...     ),
...     platinum=Sum(
...         Case(When(plan=Sponsor.PLATINUM, then=1),
...              output_field=IntegerField())
...     )
... )
{'platinum': 3, 'silver': 3, 'gold': None}
```

## どんなSQLが作られているのか？

[djnago-debug-toolbar](https://pypi.python.org/pypi/django-debug-toolbar)の`debugsqlshell`コマンドを使って、実際に作られるSQLを見てみましょう。[^2]

```pycon
>>> Sponsor.objects.annotate(
...     fee=Case(
...         When(plan=Sponsor.DIAMOND, then=Value(u'1,000,000円 (税抜き)')),
...         When(plan=Sponsor.PLATINUM, then=Value(u'500,000円 (税抜き)')),
...         When(plan=Sponsor.GOLD, then=Value(u'300,000円 (税抜き)')),
...         When(plan=Sponsor.SILVER, then=Value(u'100,000円 (税抜き)')),
...         output_field=CharField(),
...     ),
... ).values_list('name', 'fee')
SELECT `sponsors_sponsor`.`name`,
       CASE
           WHEN `sponsors_sponsor`.`plan` = 'D' THEN '1,000,000円 (税抜き)'
           WHEN `sponsors_sponsor`.`plan` = 'P' THEN '500,000円 (税抜き)'
           WHEN `sponsors_sponsor`.`plan` = 'G' THEN '300,000円 (税抜き)'
           WHEN `sponsors_sponsor`.`plan` = 'S' THEN '100,000円 (税抜き)'
           ELSE NULL
       END AS `fee`
FROM `sponsors_sponsor` LIMIT 21 [0.23ms]

>>> Sponsor.objects.update(
...     plan=Case(
...         When(registered_on__lte=a_week_ago ,
...              then=Value(Sponsor.PLATINUM)),
...         When(registered_on__lte=a_month_ago,
...              then=Value(Sponsor.GOLD)),
...         default=Value(Sponsor.SILVER)
...     ),
... )
UPDATE `sponsors_sponsor`
SET `plan` = CASE
                 WHEN `sponsors_sponsor`.`registered_on` <= '2015-04-22' THEN 'P'
                 WHEN `sponsors_sponsor`.`registered_on` <= '2015-03-30' THEN 'G'
                 ELSE 'S'
             END [0.62ms]

>>> Sponsor.objects.aggregate(
...     silver=Sum(
...         Case(When(plan=Sponsor.SILVER, then=1),
...              output_field=IntegerField())
...     ),
...     gold=Sum(
...         Case(When(plan=Sponsor.GOLD, then=1),
...              output_field=IntegerField())
...     ),
...     platinum=Sum(
...         Case(When(plan=Sponsor.PLATINUM, then=1),
...              output_field=IntegerField())
...     )
... )
SELECT SUM(CASE WHEN `sponsors_sponsor`.`plan` = 'P' THEN 1 ELSE NULL END) AS `platinum`,
       SUM(CASE WHEN `sponsors_sponsor`.`plan` = 'S' THEN 1 ELSE NULL END) AS `silver`,
       SUM(CASE WHEN `sponsors_sponsor`.`plan` = 'G' THEN 1 ELSE NULL END) AS `gold`
FROM `sponsors_sponsor` [0.32ms]
```

[^1]: [本家ドキュメントのサンプルコード](https://docs.djangoproject.com/en/1.8/ref/models/conditional-expressions/)を[PyCon JP 2015スポンサーシップのご案内](https://pycon.jp/2015/sponsors/prospectus.html)を参考に改変。
[^2]: データベースはMySQL5.5.43を使っています。