条件式¶
条件式を使用すると、フィルタ、アノテーション、集計、更新の中で if
... elif
... else
ロジックを使用できます。条件式はテーブルの各行に対して一連の条件を評価し、マッチした結果式を返します。条件式は他の 式 のように組み合わせたり、入れ子にしたりすることもできます。
条件式クラス¶
このページの例では、以下のモデルを使用します:
from django.db import models
class Client(models.Model):
REGULAR = "R"
GOLD = "G"
PLATINUM = "P"
ACCOUNT_TYPE_CHOICES = {
REGULAR: "Regular",
GOLD: "Gold",
PLATINUM: "Platinum",
}
name = models.CharField(max_length=50)
registered_on = models.DateField()
account_type = models.CharField(
max_length=1,
choices=ACCOUNT_TYPE_CHOICES,
default=REGULAR,
)
When
¶
When()
オブジェクトは、条件式で使用する条件とその結果をカプセル化するために使用します。When()
オブジェクトの使用方法は filter()
メソッドの使用方法と似ています。条件は フィルドルックアップ や Q
オブジェクト、あるいは BooleanField
である output_field
を持つ Expression
オブジェクトを使って指定できます。結果は then
キーワードで指定します。
例:
>>> from django.db.models import F, Q, When
>>> # String arguments refer to fields; the following two examples are equivalent:
>>> When(account_type=Client.GOLD, then="name")
>>> When(account_type=Client.GOLD, then=F("name"))
>>> # You can use field lookups in the condition
>>> from datetime import date
>>> When(
... registered_on__gt=date(2014, 1, 1),
... registered_on__lt=date(2015, 1, 1),
... then="account_type",
... )
>>> # Complex conditions can be created using Q objects
>>> When(Q(name__startswith="John") | Q(name__startswith="Paul"), then="name")
>>> # Condition can be created using boolean expressions.
>>> from django.db.models import Exists, OuterRef
>>> non_unique_account_type = (
... Client.objects.filter(
... account_type=OuterRef("account_type"),
... )
... .exclude(pk=OuterRef("pk"))
... .values("pk")
... )
>>> When(Exists(non_unique_account_type), then=Value("non unique"))
>>> # Condition can be created using lookup expressions.
>>> from django.db.models.lookups import GreaterThan, LessThan
>>> When(
... GreaterThan(F("registered_on"), date(2014, 1, 1))
... & LessThan(F("registered_on"), date(2015, 1, 1)),
... then="account_type",
... )
これらの値はそれぞれ式になることを覚えておいてください。
注釈
then
キーワード引数は When()
の結果のために予約されているので、 Model
に then
という名前のフィールドがある場合、衝突する可能性があります。これは 2 つの方法で解決できます:
>>> When(then__exact=0, then=1)
>>> When(Q(then=0), then=1)
Case
¶
Case()
式は Python
の if
... elif
... else
文のようなものです。指定された When()
オブジェクトの各 condition
は 1 つが True と評価されるまで順番に評価されます。一致した When()
オブジェクトの result
式が返されます。
例:
>>>
>>> from datetime import date, timedelta
>>> from django.db.models import Case, Value, When
>>> Client.objects.create(
... name="Jane Doe",
... account_type=Client.REGULAR,
... registered_on=date.today() - timedelta(days=36),
... )
>>> Client.objects.create(
... name="James Smith",
... account_type=Client.GOLD,
... registered_on=date.today() - timedelta(days=5),
... )
>>> Client.objects.create(
... name="Jack Black",
... account_type=Client.PLATINUM,
... registered_on=date.today() - timedelta(days=10 * 365),
... )
>>> # Get the discount for each Client based on the account type
>>> Client.objects.annotate(
... discount=Case(
... When(account_type=Client.GOLD, then=Value("5%")),
... When(account_type=Client.PLATINUM, then=Value("10%")),
... default=Value("0%"),
... ),
... ).values_list("name", "discount")
<QuerySet [('Jane Doe', '0%'), ('James Smith', '5%'), ('Jack Black', '10%')]>
Case()
は任意の数の When()
オブジェクトを引数として受け取ることができます。その他のオプションはキーワード引数で指定します。どの条件も TRUE
と評価されない場合、キーワード引数 default
で与えられた式が返されます。もし default
引数が与えられなかった場合、 None
が使用されます。
先ほどのクエリを修正して、Client
が私たちとどれくらいの期間、関係があるかに基づいて割引を得るには、ルックアップが使用できます:
>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> # Get the discount for each Client based on the registration date
>>> Client.objects.annotate(
... discount=Case(
... When(registered_on__lte=a_year_ago, then=Value("10%")),
... When(registered_on__lte=a_month_ago, then=Value("5%")),
... default=Value("0%"),
... )
... ).values_list("name", "discount")
<QuerySet [('Jane Doe', '5%'), ('James Smith', '0%'), ('Jack Black', '10%')]>
注釈
条件は順番に評価されるので、上の例では2番目の条件がJane DoeとJack Blackの両方にマッチしても正しい結果が得られることを覚えておいてください。これは Python
の if
... elif
... else
文と同じように動作します。
case()
は filter()
句でも使えます。たとえば、1ヶ月以上前に登録されたゴールドクライアントと1年以上前に登録されたプラチナクライアントを検索する場合:
>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> Client.objects.filter(
... registered_on__lte=Case(
... When(account_type=Client.GOLD, then=a_month_ago),
... When(account_type=Client.PLATINUM, then=a_year_ago),
... ),
... ).values_list("name", "account_type")
<QuerySet [('Jack Black', 'P')]>
高度なクエリ¶
条件式は、アノテーション、集計、フィルタ、ルックアップ、および更新で使用できます。また、他の式と組み合わせたり、入れ子にしたりすることもできます。これにより、強力な条件クエリを作成できます。
条件付きの更新¶
例えば、クライアントの account_type
を登録日とマッチするように変更したいとします。これを行うには、条件式と update()
メソッドを使います:
>>> a_month_ago = date.today() - timedelta(days=30)
>>> a_year_ago = date.today() - timedelta(days=365)
>>> # Update the account_type for each Client from the registration date
>>> Client.objects.update(
... account_type=Case(
... When(registered_on__lte=a_year_ago, then=Value(Client.PLATINUM)),
... When(registered_on__lte=a_month_ago, then=Value(Client.GOLD)),
... default=Value(Client.REGULAR),
... ),
... )
>>> Client.objects.values_list("name", "account_type")
<QuerySet [('Jane Doe', 'G'), ('James Smith', 'R'), ('Jack Black', 'P')]>
条件付きの集計¶
それぞれの``account_type``に対応するクライアントの数を知りたい場合はどうすればよいでしょうか?そのためには 集計関数 の filter
引数を使います:
>>> # Create some more Clients first so we can have something to count
>>> Client.objects.create(
... name="Jean Grey", account_type=Client.REGULAR, registered_on=date.today()
... )
>>> Client.objects.create(
... name="James Bond", account_type=Client.PLATINUM, registered_on=date.today()
... )
>>> Client.objects.create(
... name="Jane Porter", account_type=Client.PLATINUM, registered_on=date.today()
... )
>>> # Get counts for each value of account_type
>>> from django.db.models import Count
>>> Client.objects.aggregate(
... regular=Count("pk", filter=Q(account_type=Client.REGULAR)),
... gold=Count("pk", filter=Q(account_type=Client.GOLD)),
... platinum=Count("pk", filter=Q(account_type=Client.PLATINUM)),
... )
{'regular': 2, 'gold': 1, 'platinum': 3}
この集計は、SQL 2003の FILTER WHERE
構文をサポートするデータベース上で、クエリを生成します:
SELECT count('id') FILTER (WHERE account_type=1) as regular,
count('id') FILTER (WHERE account_type=2) as gold,
count('id') FILTER (WHERE account_type=3) as platinum
FROM clients;
他のデータベースでは、これは CASE
ステートメントを使ってエミュレートされます:
SELECT count(CASE WHEN account_type=1 THEN id ELSE null) as regular,
count(CASE WHEN account_type=2 THEN id ELSE null) as gold,
count(CASE WHEN account_type=3 THEN id ELSE null) as platinum
FROM clients;
この2つのSQL文は機能的には等価ですが、より明示的な FILTER
の方がパフォーマンスが良いかもしれません。
条件フィルタ¶
条件フィルタ条件式が真偽値を返す場合、それを直接フィルタで使用できます。つまり、 SELECT
カラムには追加されませんが、結果のフィルタリングには使用できます:
>>> non_unique_account_type = (
... Client.objects.filter(
... account_type=OuterRef("account_type"),
... )
... .exclude(pk=OuterRef("pk"))
... .values("pk")
... )
>>> Client.objects.filter(~Exists(non_unique_account_type))
SQL文では、次のように評価されます:
SELECT ...
FROM client c0
WHERE NOT EXISTS (
SELECT c1.id
FROM client c1
WHERE c1.account_type = c0.account_type AND NOT c1.id = c0.id
)