Django ORMの逆参照入門
1側から多側のデータを取得する方法
Django ORMで1側のモデルから、ForeignKeyで紐づく多側のデータを取得する逆参照の方法を解説します。
こんな人向けの記事です
- 1対多の「1側」から関連データを取得したい人
- related_nameや_setの使い方を知りたい人
- prefetch_relatedでN+1問題を解決したい人
Step 1逆参照の基本
ForeignKeyを定義すると、1側のモデルから多側のデータを取得するための「逆参照マネージャー」が自動的に作られます。
Python
# models.py
from django.db import models
class Department(models.Model):
"""部署(1側)"""
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Employee(models.Model):
"""社員(多側)"""
name = models.CharField(max_length=100)
department = models.ForeignKey(
Department,
on_delete=models.CASCADE
)
def __str__(self):
return self.namePython
# 部署(1側)を取得
dept = Department.objects.get(name="営業部")
# 逆参照: 部署に所属する社員を全件取得
# デフォルトでは「モデル名小文字_set」という名前
employees = dept.employee_set.all()
for emp in employees:
print(f" {emp.name}")
# 件数を取得
print(f"営業部の社員数: {dept.employee_set.count()}人")実行結果
田中太郎
佐藤花子
山田次郎
営業部の社員数: 3人逆参照マネージャーは通常のマネージャーと同じように、all()、filter()、count()、order_by()などのメソッドが使えます。
Step 2related_nameのカスタマイズ
ForeignKeyのrelated_name引数で、逆参照の名前を変更できます。
Python
# models.py
class Employee(models.Model):
name = models.CharField(max_length=100)
department = models.ForeignKey(
Department,
on_delete=models.CASCADE,
related_name="employees" # 逆参照の名前をカスタマイズ
)
def __str__(self):
return self.namePython
# related_name="employees" を設定した場合
dept = Department.objects.get(name="営業部")
# employee_set の代わりに employees が使える
employees = dept.employees.all()
print(f"社員数: {dept.employees.count()}人")
# filter()も使える
senior = dept.employees.filter(age__gte=30)
# 逆参照を無効にする場合
# related_name="+" # +を指定すると逆参照を作らないrelated_nameの命名規則
一般的にrelated_nameには関連モデルの複数形を使います。例: employees、orders、comments。コードの可読性が大幅に向上します。Step 3prefetch_relatedでN+1問題を解決
1側から多側のデータを取得する場合は、prefetch_related()を使ってN+1問題を防ぎます。
Python
# BAD: N+1問題(部署数+1回のSQLが発行される)
departments = Department.objects.all()
for dept in departments:
# 毎回Employeeテーブルへのクエリが発行される
employees = dept.employees.all()
print(f"{dept.name}: {employees.count()}人")
# SQL: SELECT * FROM department
# SQL: SELECT * FROM employee WHERE department_id = 1
# SQL: SELECT * FROM employee WHERE department_id = 2
# ...Python
# GOOD: prefetch_related()で2回のSQLで済む
departments = Department.objects.prefetch_related("employees").all()
for dept in departments:
# 追加のクエリは発行されない(プリフェッチ済み)
employees = dept.employees.all()
print(f"{dept.name}: {employees.count()}人")
# SQL: SELECT * FROM department
# SQL: SELECT * FROM employee WHERE department_id IN (1, 2, 3, ...)実行結果
営業部: 3人
開発部: 5人
人事部: 2人Python
# Prefetchオブジェクトで条件付きプリフェッチ
from django.db.models import Prefetch
# アクティブな社員だけをプリフェッチ
departments = Department.objects.prefetch_related(
Prefetch(
"employees",
queryset=Employee.objects.filter(is_active=True),
to_attr="active_employees" # 属性名を指定
)
)
for dept in departments:
print(f"{dept.name}: {len(dept.active_employees)}人(アクティブ)")select_relatedとprefetch_relatedの違い
select_related: ForeignKey(多→1)に使う。JOINで1回のSQLに統合。prefetch_related: 逆参照(1→多)やManyToManyに使う。別クエリでIN句を使って取得。Step 4逆参照でのフィルタリングと集計
Python
from django.db.models import Count, Avg, Q
# 社員が5人以上いる部署を取得
departments = Department.objects.annotate(
emp_count=Count("employees")
).filter(emp_count__gte=5)
# 各部署の平均年齢を集計
departments = Department.objects.annotate(
avg_age=Avg("employees__age")
).order_by("avg_age")
for dept in departments:
print(f"{dept.name}: 平均 {dept.avg_age:.1f}歳")
# 逆参照を使ったフィルタリング(JOINが発生)
# 30歳以上の社員がいる部署を取得
departments = Department.objects.filter(employees__age__gte=30).distinct()
# 社員がいない部署を取得
departments = Department.objects.filter(employees__isnull=True)Step 5実践的な使用例
Python
# views.py
from django.shortcuts import render, get_object_or_404
from django.db.models import Count, Prefetch
from .models import Department, Employee
def department_list(request):
departments = Department.objects.annotate(
employee_count=Count("employees")
).prefetch_related(
Prefetch(
"employees",
queryset=Employee.objects.order_by("name")[:3],
to_attr="top_employees"
)
).order_by("name")
return render(request, "department/list.html", {
"departments": departments
})
def department_detail(request, pk):
department = get_object_or_404(Department, pk=pk)
# 部署の社員を取得(逆参照)
employees = department.employees.order_by("-hired_date")
return render(request, "department/detail.html", {
"department": department,
"employees": employees,
"total": employees.count(),
})まとめ
- 1側からは
モデル名小文字_set(またはrelated_name)で多側のデータにアクセスできる related_nameで逆参照の名前をカスタマイズすると可読性が向上するprefetch_related()でN+1問題を防ぎ、効率的にデータを取得する- Prefetchオブジェクトで条件付きプリフェッチが可能
- annotate()と組み合わせて集計やフィルタリングができる