Django ORMのForeignKey入門
多側から1側のデータを取得する方法
Django ORMのForeignKeyリレーションで、多側のモデルから1側(親)のデータにアクセスする方法を解説します。
こんな人向けの記事です
- Django ORMのリレーション(外部キー)の基本を学びたい人
- 多対1の関係でデータを取得する方法を知りたい人
- N+1問題とselect_relatedを理解したい人
Step 1ForeignKeyの基本
ForeignKeyは多対1のリレーションを定義します。「多」側のモデルにForeignKeyフィールドを設定します。
Python
# models.py
from django.db import models
class Department(models.Model):
"""部署(1側)"""
name = models.CharField(max_length=100)
location = models.CharField(max_length=100)
def __str__(self):
return self.name
class Employee(models.Model):
"""社員(多側)- 1つの部署に複数の社員が所属"""
name = models.CharField(max_length=100)
email = models.EmailField()
department = models.ForeignKey(
Department,
on_delete=models.CASCADE, # 部署削除時に社員も削除
related_name="employees" # 逆参照の名前
)
def __str__(self):
return self.namerelated_nameとは
related_nameは1側(Department)から多側(Employee)を逆参照するときの名前です。省略するとemployee_setというデフォルト名が使われます。Step 2多側から1側のデータにアクセス
ForeignKeyフィールドを通じて、関連する1側のモデルインスタンスに直接アクセスできます。
Python
# 社員を取得
employee = Employee.objects.get(name="田中太郎")
# 多側(Employee)から1側(Department)にアクセス
print(employee.department) # 営業部(Departmentインスタンス)
print(employee.department.name) # 営業部
print(employee.department.location) # 東京本社
# ForeignKeyのIDだけを取得(SQLを発行しない)
print(employee.department_id) # 1(データベースのFK値を直接取得)実行結果
営業部
営業部
東京本社
1department vs department_id
employee.departmentはDepartmentインスタンスを返すため、初回アクセス時にSQLが発行されます。IDだけが必要な場合はemployee.department_idを使うとSQLが発行されません。Step 3select_relatedでN+1問題を解決
ループ内でForeignKeyにアクセスすると、ループの回数だけSQLが発行されます(N+1問題)。select_related()で事前にJOINして解決できます。
Python
# BAD: N+1問題(社員数+1回のSQLが発行される)
employees = Employee.objects.all()
for emp in employees:
# 毎回Departmentテーブルへのクエリが発行される
print(f"{emp.name} - {emp.department.name}")
# SQL: SELECT * FROM employee
# SQL: SELECT * FROM department WHERE id = 1 ← N回繰り返し
# SQL: SELECT * FROM department WHERE id = 2
# ...Python
# GOOD: select_related()でJOIN(1回のSQLで済む)
employees = Employee.objects.select_related("department").all()
for emp in employees:
# 追加のクエリは発行されない
print(f"{emp.name} - {emp.department.name}")
# SQL: SELECT employee.*, department.*
# FROM employee
# INNER JOIN department ON employee.department_id = department.id実行結果
田中太郎 - 営業部
佐藤花子 - 開発部
鈴木一郎 - 営業部Python
# 複数のForeignKeyをまとめてselect_related
class Employee(models.Model):
department = models.ForeignKey(Department, on_delete=models.CASCADE)
position = models.ForeignKey(Position, on_delete=models.CASCADE)
# 複数のFKを一度にJOIN
employees = Employee.objects.select_related(
"department", "position"
).all()
# ネストしたForeignKeyも対応(二重アンダースコアで指定)
# Department → Company のようなチェーンの場合
employees = Employee.objects.select_related(
"department__company"
).all()Step 4関連モデルでのフィルタリング
ForeignKeyの先のフィールドで条件を指定するには、ダブルアンダースコア(__)を使います。
Python
# 部署名が"営業部"の社員を取得
employees = Employee.objects.filter(department__name="営業部")
# 部署の所在地が"東京"を含む社員を取得
employees = Employee.objects.filter(
department__location__contains="東京"
)
# select_relatedと組み合わせ
employees = Employee.objects.select_related(
"department"
).filter(
department__name__in=["営業部", "開発部"]
).order_by("department__name", "name")実行結果
<QuerySet [<Employee: 田中太郎>, <Employee: 鈴木一郎>]>Step 5実践的な使用例
Python
# views.py
from django.shortcuts import render, get_object_or_404
from .models import Employee
def employee_list(request):
# select_relatedで部署情報も一緒に取得
employees = Employee.objects.select_related(
"department"
).order_by("department__name", "name")
# 部署でフィルタ
dept = request.GET.get("department")
if dept:
employees = employees.filter(department__name=dept)
return render(request, "employee/list.html", {
"employees": employees
})
def employee_detail(request, pk):
# get_object_or_404でもselect_relatedは使える
employee = get_object_or_404(
Employee.objects.select_related("department"),
pk=pk
)
return render(request, "employee/detail.html", {
"employee": employee,
"department": employee.department, # 追加クエリなし
})まとめ
- ForeignKeyフィールドを通じて多側から1側のデータに直接アクセスできる
- IDだけ必要な場合は
field_idを使うとSQLが発行されない select_related()でN+1問題を防ぎ、JOINで効率的に取得する- ダブルアンダースコア(
department__name)で関連モデルのフィールドでフィルタできる - ループ内でForeignKeyにアクセスする場合は必ず
select_related()を使う