ORM

Django ORMで条件分岐|Case/When式の使い方

DjangoのORMでSQLのCASE WHEN文に相当する条件分岐を行うには、CaseWhenを使用します。条件に応じて異なる値を返したり、条件ごとに計算を変えたりする処理をデータベース側で効率的に実行できます。

基本的な使い方

views.py
sales_exists = Sales.objects.filter(name=OuterRef('pk'))

person_model = Person.objects.annotate(
    has_sales=Case(
        When(Exists(sales_exists), then=Value('売り上げあり')),
        default=Value('売り上げなし'),
    )
)

print(person_model.values('name', 'has_sales'))

説明

Step 1Case/When式の基本

Djangoでは、Case/When式を使用してORM内で条件分岐を行うことができます。基本的な構文は以下の通りです:

from django.db.models import Case, When, Value

Case(
   When(条件, then=条件に一致したときの値),
   default=条件に一致しなかったときの値,
)

この式はSQL文の「CASE WHEN...THEN...ELSE...END」に相当し、データベースレベルで条件分岐を実行します。

Step 2基本的な使用例

例えば、Personモデルに紐づいているSalesの有無によって値を分岐させる場合:

from django.db.models import Case, When, Value, Count, CharField

# Personモデルに、Salesが存在するかどうかのフラグを追加
persons = Person.objects.annotate(
    sales_count=Count('sales'),
    has_sales=Case(
        When(sales_count__gt=0, then=Value('売り上げあり')),
        default=Value('売り上げなし'),
        output_field=CharField()
    )
)

上の例では、自身に紐づいたSalesが1つでもあれば「売り上げあり」、なければ「売り上げなし」がhas_salesフィールドに代入されています。

Step 3複数の条件分岐

When句を複数使って、複数の条件分岐を作ることもできます:

from django.db.models import Case, When, Value, IntegerField

# 売上金額に応じてランク付け
persons = Person.objects.annotate(
    total_sales=Sum('sales__amount'),
    sales_rank=Case(
        When(total_sales__gte=1000000, then=Value(1)),  # 100万以上はランク1
        When(total_sales__gte=500000, then=Value(2)),   # 50万以上はランク2
        When(total_sales__gte=100000, then=Value(3)),   # 10万以上はランク3
        default=Value(4),                               # それ以下はランク4
        output_field=IntegerField()
    )
)

When句は上から順に評価され、最初に条件が一致したところのthen値が採用されます。

Step 4フィルタリングと組み合わせる

Case/When式の結果でフィルタリングすることもできます:

# ランク1とランク2の人だけを取得
top_performers = Person.objects.annotate(
    total_sales=Sum('sales__amount'),
    sales_rank=Case(
        When(total_sales__gte=1000000, then=Value(1)),
        When(total_sales__gte=500000, then=Value(2)),
        When(total_sales__gte=100000, then=Value(3)),
        default=Value(4),
        output_field=IntegerField()
    )
).filter(sales_rank__lte=2)

Step 5様々な条件式の例

Case/When式ではさまざまな条件を使用できます:

# 文字列フィールドに基づく条件
persons = Person.objects.annotate(
    department_category=Case(
        When(department__startswith='営業', then=Value('営業系')),
        When(department__startswith='技術', then=Value('技術系')),
        When(department__startswith='管理', then=Value('管理系')),
        default=Value('その他'),
        output_field=CharField()
    )
)

# 複数フィールドの組み合わせ条件
from django.db.models import Q

persons = Person.objects.annotate(
    status=Case(
        When(Q(age__gte=60) & Q(years_of_service__gte=20), then=Value('定年退職対象')),
        When(Q(age__gte=50) & Q(years_of_service__gte=15), then=Value('早期退職可能')),
        default=Value('通常雇用'),
        output_field=CharField()
    )
)

Step 6実践的な使用例

views.pyでのCase/When式の使用例:

from django.shortcuts import render
from django.db.models import Case, When, Value, Sum, Count, CharField, IntegerField
from .models import Person, Sales

def sales_analysis(request):
    # 売上実績に基づく分析
    persons = Person.objects.annotate(
        # 売上件数
        sales_count=Count('sales'),
        
        # 売上合計
        total_sales=Sum('sales__amount'),
        
        # 売上状況の分類
        sales_status=Case(
            When(sales_count=0, then=Value('未売上')),
            When(sales_count__gte=10, then=Value('優良営業')),
            default=Value('通常営業'),
            output_field=CharField()
        ),
        
        # 売上金額に基づくランク
        sales_rank=Case(
            When(total_sales__gte=1000000, then=Value('S')),
            When(total_sales__gte=500000, then=Value('A')),
            When(total_sales__gte=100000, then=Value('B')),
            When(total_sales__gt=0, then=Value('C')),
            default=Value('D'),
            output_field=CharField()
        ),
        
        # 売上達成率に応じたボーナス計算
        bonus_percentage=Case(
            When(total_sales__gte=2000000, then=Value(20)),  # 200万以上は20%ボーナス
            When(total_sales__gte=1000000, then=Value(15)),  # 100万以上は15%ボーナス
            When(total_sales__gte=500000, then=Value(10)),   # 50万以上は10%ボーナス
            When(total_sales__gt=0, then=Value(5)),          # 売上あれば5%ボーナス
            default=Value(0),
            output_field=IntegerField()
        )
    ).order_by('-total_sales')
    
    return render(request, 'persons/sales_analysis.html', {
        'persons': persons
    })

テンプレートでの使用例(sales_analysis.html):

<h1>営業担当者売上分析</h1>

<table>
    <tr>
        <th>担当者名</th>
        <th>売上件数</th>
        <th>売上合計</th>
        <th>売上状況</th>
        <th>ランク</th>
        <th>ボーナス率</th>
    </tr>
    {% for person in persons %}
        <tr>
            <td>{{ person.name }}</td>
            <td>{{ person.sales_count }}件</td>
            <td>{{ person.total_sales|default:0|floatformat:0 }}円</td>
            <td>{{ person.sales_status }}</td>
            <td>{{ person.sales_rank }}</td>
            <td>{{ person.bonus_percentage }}%</td>
        </tr>
    {% endfor %}
</table>
重要ポイント:
  • Case/When式はデータベースレベルで実行されるため、Pythonコードで条件分岐するよりも効率的です。
  • output_fieldパラメータで、結果の型を指定する必要があります(CharFieldやIntegerFieldなど)。
  • 条件が複雑な場合はQオブジェクトを使用して柔軟な条件式を作成できます。
  • When句は上から順に評価されるため、条件の順序が重要です(最初に合致した条件のthen値が採用されます)。
  • defaultを指定しないと、どの条件にも一致しない場合にNoneが返されます。

まとめ

  • Case/WhenでSQLのCASE WHEN文をORM内で表現できる
  • When(条件, then=値)で条件と返す値を指定する
  • default引数でどの条件にも合わない場合の値を指定できる
  • output_fieldで結果のデータ型を明示的に指定する
  • annotate()と組み合わせて条件別の計算フィールドを追加できる
  • filter()order_by()内でも使用可能