ORM

Django ORMの逆参照入門|1側から多側のデータを取得する方法

Django ORM

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.name
Python
# 部署(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.name
Python
# 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には関連モデルの複数形を使います。例: employeesorderscomments。コードの可読性が大幅に向上します。

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()と組み合わせて集計やフィルタリングができる