Django ORMで紐づいた別のモデルのフィールドを結合して取得する方法を解説します。select_related()を使うとSQLのJOINを発行し、関連モデルのデータを1回のクエリで効率的に取得できます。
基本的な使い方
ForeignKeyで紐づいたモデルのデータを結合して取得するにはselect_related()を使います。
class Employee(models.Model):
name = models.CharField(max_length=100)
department = models.ForeignKey('Department', on_delete=models.CASCADE)
class Sale(models.Model):
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
amount = models.IntegerField()
sales_date = models.DateField()from .models import Sale
def index(request):
# SaleとEmployeeを結合して取得
sales = Sale.objects.select_related('employee')
for sale in sales:
# 追加クエリなしでemployeeのフィールドにアクセスできる
print(f"{sale.employee.name}: {sale.amount}円")山田太郎: 15000円
山田太郎: 22000円
佐藤花子: 18000円select_related()を使わない場合、ループ内でsale.employeeにアクセスするたびに追加のSQLクエリが発行されてしまいます(N+1問題)。
select_relatedなしの場合(N+1問題)
なぜselect_related()が重要なのかを理解するため、使わない場合を見てみましょう。
# N+1問題が発生するコード
sales = Sale.objects.all() # 1回目のクエリ
for sale in sales:
print(sale.employee.name) # ループごとに追加クエリが発行される!
# Sale 100件 → 合計101回のクエリSELECT * FROM sale; -- 1回目
SELECT * FROM employee WHERE id = 1; -- 2回目
SELECT * FROM employee WHERE id = 2; -- 3回目
... -- N回繰り返しSELECT sale.*, employee.*
FROM sale
INNER JOIN employee ON sale.employee_id = employee.id; -- 1回だけ!複数のモデルを結合
2つ以上のForeignKeyを結合することもできます。
# 複数のForeignKeyを同時に結合
sales = Sale.objects.select_related('employee', 'product')
# ネストした関連の結合(EmployeeのDepartmentまで結合)
sales = Sale.objects.select_related('employee__department')
for sale in sales:
print(f"{sale.employee.name} ({sale.employee.department.name}): {sale.amount}円")employee__departmentのようにダブルアンダースコアで深い階層のリレーションも結合できます。
valuesと組み合わせてフィールドを選択
結合したモデルの特定フィールドだけを取得するにはvalues()を組み合わせます。
# 関連モデルのフィールドをvaluesで取得
sales = Sale.objects.values(
'employee__name',
'amount',
'sales_date'
)
# [{'employee__name': '山田太郎', 'amount': 15000, 'sales_date': ...}, ...]
# annotateで別名を付ける
from django.db.models import F
sales = Sale.objects.annotate(
employee_name=F('employee__name')
).values('employee_name', 'amount', 'sales_date')prefetch_relatedとの使い分け
select_related()とprefetch_related()は用途が異なります。
# select_related: ForeignKey、OneToOneField(JOINを使う)
sales = Sale.objects.select_related('employee')
# prefetch_related: ManyToMany、逆参照(別クエリで取得してPythonで結合)
employees = Employee.objects.prefetch_related('sale_set')select_relatedはSQL JOINで1回のクエリで取得します。ForeignKeyとOneToOneFieldに対して使います。prefetch_relatedは別クエリで取得しPython側で結合します。ManyToManyや逆参照(_set)に対して使います。
条件付き結合
結合したモデルのフィールドでフィルタリングも可能です。
# 営業部の社員の売上のみ取得
sales = Sale.objects.select_related('employee__department').filter(
employee__department__name="営業部"
)
# 金額範囲で絞り込み
sales = Sale.objects.select_related('employee').filter(
amount__gte=10000,
employee__name__contains="山田"
)実践的な使用例
売上レポート画面を構築する実践例です。
from django.shortcuts import render
from django.db.models import Sum, Count
from .models import Employee, Sale
def performance_report(request):
# 各社員の売上実績を取得(annotateと組み合わせ)
top_sellers = Employee.objects.annotate(
total_sales=Sum('sale__amount'),
sales_count=Count('sale')
).filter(
total_sales__isnull=False
).order_by('-total_sales')[:10]
# 売上のない社員も含めたレポート
all_employees = Employee.objects.select_related(
'department'
).annotate(
total_sales=Sum('sale__amount'),
sales_count=Count('sale')
).order_by('department__name', 'name')
return render(request, 'reports/performance.html', {
'top_sellers': top_sellers,
'all_employees': all_employees,
})select_related()はINNER JOINではなくLEFT OUTER JOINを使用します。そのため、関連データがないレコードも取得されます。関連データが必ず存在するレコードだけに絞りたい場合は、.filter(employee__isnull=False)を追加してください。
まとめ
select_related()でForeignKey先のデータをJOINで1回のクエリで取得できる- N+1問題(ループ内で追加クエリが発生する問題)を防げる
- ダブルアンダースコアで深い階層のリレーションも結合可能
- ManyToManyや逆参照には
prefetch_related()を使う values()やannotate()と組み合わせて柔軟にデータを取得できる