DjangoのORMで、あるモデルのクエリ結果を別のクエリの条件として使うには、Subqueryを使用します。SQLのサブクエリに相当する機能で、複雑なデータ取得を実現できます。
基本的な使い方
views.py
from django.db.models import Subquery, OuterRef
last_sales_date = Sales.objects.filter(
name=OuterRef('pk')
).order_by('-date').values('date')[:1]
model = Person.objects.annotate(
last_sale_date=Subquery(last_sales_date))
print(model.values('name', 'last_sale_date'))
説明
Step 1SubqueryとOuterRefの基本
Djangoでは、SubqueryとOuterRefを組み合わせることで、自身を呼び出しているデータを使用してフィルタリングし、そのデータをannotateで追加することができます。
from django.db.models import Subquery, OuterRef
重要な注意点: Subqueryは複数データがある状態が返ってくるとエラーになるので、返ってくるデータが1つになるようにする必要があります。
Step 2基本的な使用例
例えば、各Personに対して最終売上日を取得する場合:
from django.db.models import Subquery, OuterRef
# 各人の最終売上日を取得するサブクエリ
last_sales_date = Sales.objects.filter(
person_id=OuterRef('pk') # 外部参照で現在処理中のPersonのpkを参照
).order_by('-date').values('date')[:1] # 日付の降順で並べて最初の1件だけ取得
# Personに最終売上日を追加
persons = Person.objects.annotate(
last_sale_date=Subquery(last_sales_date)
)
上の例は、OuterRefを使用して、自身を呼び出しているモデル(Person)のpkを使用してフィルタリングしています。last_sales_dateはperson_idフィールドが自身を呼び出しているモデルのpkのデータを取得し、それをdateで降順にし、最初の1件目を取得しています。
Subqueryはannotateに他のモデルのデータを埋め込むのに使用され、ここでは、変数last_sales_dateで処理された結果がlast_sale_dateフィールドに代入されています。
Step 3複雑な例:フィールドの取得と条件付きサブクエリ
特定のフィールドの値や、条件付きのデータを取得する例:
# 各人の最終売上額を取得
last_sale_amount = Sales.objects.filter(
person_id=OuterRef('pk')
).order_by('-date').values('amount')[:1]
# 各人の今年の最高売上額を取得
from django.db.models import Max
from datetime import datetime
current_year = datetime.now().year
max_sale_this_year = Sales.objects.filter(
person_id=OuterRef('pk'),
date__year=current_year
).values('person_id').annotate(
max_amount=Max('amount')
).values('max_amount')
# Personに最終売上額と今年の最高売上額を追加
persons = Person.objects.annotate(
last_sale_amount=Subquery(last_sale_amount),
max_sale_this_year=Subquery(max_sale_this_year)
)
Step 4複数のモデルにまたがるサブクエリ
複数のモデルを通じた関連データの取得:
# 各会社の最終売上の担当者名を取得
latest_sale_person = Sales.objects.filter(
company_id=OuterRef('pk')
).order_by('-date').values('person__name')[:1]
# 各会社の売上合計が最も多い部門名を取得
top_department = Sales.objects.filter(
company_id=OuterRef('pk')
).values('department').annotate(
total=Sum('amount')
).order_by('-total').values('department')[:1]
# Companyに最終売上担当者と売上トップ部門を追加
companies = Company.objects.annotate(
latest_sale_by=Subquery(latest_sale_person),
top_department=Subquery(top_department)
)
Step 5条件分岐を含むサブクエリ
Case式とSubqueryを組み合わせて、条件分岐を含む集計を行う例:
from django.db.models import Case, When, Value, CharField
# 各人の売上状況に基づくステータスを計算
has_sales = Sales.objects.filter(
person_id=OuterRef('pk')
).values('person_id').annotate(
count=Count('id')
).values('count')[:1]
# Personにステータスを追加
persons = Person.objects.annotate(
sales_status=Case(
When(Subquery(has_sales) > 0, then=Value('有効')),
default=Value('未売上'),
output_field=CharField()
)
)
Step 6実践的な使用例
views.pyでのSubqueryとOuterRefの使用例:
from django.shortcuts import render
from django.db.models import Subquery, OuterRef, Sum, Count, F, ExpressionWrapper, FloatField
from .models import Person, Sales, Company
def sales_dashboard(request):
# 各人の最終売上情報
last_sale_date = Sales.objects.filter(
person_id=OuterRef('pk')
).order_by('-date').values('date')[:1]
last_sale_amount = Sales.objects.filter(
person_id=OuterRef('pk')
).order_by('-date').values('amount')[:1]
# 各人の売上合計と平均
total_sales = Sales.objects.filter(
person_id=OuterRef('pk')
).values('person_id').annotate(
total=Sum('amount')
).values('total')[:1]
sales_count = Sales.objects.filter(
person_id=OuterRef('pk')
).values('person_id').annotate(
count=Count('id')
).values('count')[:1]
# 平均売上額を計算(total_salesとsales_countから)
persons = Person.objects.annotate(
last_sale_date=Subquery(last_sale_date),
last_sale_amount=Subquery(last_sale_amount),
total_sales=Subquery(total_sales),
sales_count=Subquery(sales_count),
avg_sale=ExpressionWrapper(
F('total_sales') / F('sales_count'),
output_field=FloatField()
)
).order_by('-total_sales')
return render(request, 'persons/dashboard.html', {
'persons': persons
})
テンプレートでの使用例(dashboard.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.last_sale_date|default:"未売上" }}</td>
<td>{{ person.last_sale_amount|default:"-"|floatformat:0 }}円</td>
<td>{{ person.total_sales|default:0|floatformat:0 }}円</td>
<td>{{ person.sales_count|default:0 }}件</td>
<td>{{ person.avg_sale|default:"-"|floatformat:0 }}円</td>
</tr>
{% endfor %}
</table>
重要ポイント:
- SubqueryとOuterRefを使うことで、関連モデルの特定データを効率的に取得できます。
- Subqueryが返すのは必ず1件のデータにする必要があります([:1]や集計関数を使用)。
- OuterRefは現在処理中のレコードのフィールド値を参照するために使用します。
- 複数のSubqueryをannotateに追加することで、一度のクエリで複数の関連情報を取得できます。
- パフォーマンスの観点から、通常のリレーション参照よりもSubqueryを使う方が複雑なケースで効率的なことがあります。
まとめ
Subqueryで別のクエリの結果をサブクエリとして使用できるOuterRef()で外側のクエリのフィールドをサブクエリ内から参照できるannotate()と組み合わせてサブクエリの結果をフィールドとして追加できるExistsでサブクエリの存在確認ができる- 複雑な条件のフィルタリングをSQL1文で効率的に実行できる
- パフォーマンスに注意し、必要に応じてインデックスを追加する