ORM

Django ORMで逆方向リレーション|1側から多側のデータを取得する

DjangoのORMでは、ForeignKeyの逆方向アクセスにより、親モデル(1側)から子モデル(多側)のデータを取得できます。_setマネージャーやrelated_nameを使った取得方法を解説します。

基本的な使い方

views.py
model = Company.objects.prefetch_related('person').get(pk=1)

for person in model.person.all():
   print(person.name)

説明

Step 1prefetch_relatedの基本

Djangoでは、1側のモデルから多側のモデルのデータを効率的に取得するために、以下の形式でprefetch_relatedを使用します:

1側のモデル.objects.prefetch_related('リレーションのフィールド名').get(条件)
1側のモデル.objects.prefetch_related('リレーションのフィールド名').filter(条件)
1側のモデル.objects.prefetch_related('リレーションのフィールド名').all()

例えば、Companyモデル(1側)からPersonモデル(多側)のデータを取得する場合:

# Companyに関連づけられたPersonも同時に取得
company = Company.objects.prefetch_related('persons').get(id=1)

Step 2select_relatedとprefetch_relatedの違い

先ほどのselect_relatedの場合は、多側からだったので、自身が決まれば紐づいているモデルが確定したのに対して、prefetch_relatedは、1側からなので、自身が決まっても、自身に紐づいているモデルが複数あるという状態になります。

Company(1)に対してPerson(多)がある場合、1つのCompanyに紐づいているPersonは複数あるという関係です。

重要な違い:
  • select_related: 1回のSQLクエリで全データを取得(JOINを使用)
  • prefetch_related: 2回以上のSQLクエリを実行し、Pythonメモリ上で結合

Step 3紐づいたデータへのアクセス

prefetch_relatedで取得した紐づいたモデルは複数あるため、リスト形式で紐づきます。アクセスするにはselect_relatedとは違い、for文で繰り返し処理をするかインデックスを指定してアクセスすることになります:

# Companyに関連づけられたPersonを取得
company = Company.objects.prefetch_related('persons').get(id=1)

# for文でアクセス
for person in company.persons.all():
    print(person.name)

# インデックスでアクセス
first_person = company.persons.all()[0]
print(first_person.name)

Step 4N+1問題の解決

prefetch_relatedもselect_relatedと同様に、N+1問題を解決するために使用します。複数の1側モデルから多側モデルにアクセスする場合に特に効果を発揮します:

# prefetch_relatedを使わない場合(N+1問題が発生)
companies = Company.objects.all()
for company in companies:
    # 各ループでSQLが発行される
    for person in company.persons.all():
        print(f"{company.name}の社員: {person.name}")

# prefetch_relatedを使う場合(効率的)
companies = Company.objects.prefetch_related('persons').all()
for company in companies:
    # 追加のSQLは発行されない
    for person in company.persons.all():
        print(f"{company.name}の社員: {person.name}")

Step 5高度な使用例

複数の関連モデルや、ネストした関連モデルを取得することもできます:

# 複数の関連モデルを取得
companies = Company.objects.prefetch_related('persons', 'departments').all()

# ネストした関連を取得
companies = Company.objects.prefetch_related('persons__skills').all()

関連するモデルに対してフィルタリングを適用することもできます:

from django.db.models import Prefetch

# 特定条件の関連モデルのみをプリフェッチ
companies = Company.objects.prefetch_related(
    Prefetch('persons', queryset=Person.objects.filter(age__gte=30))
).all()

Step 6実践的な使用例

views.pyでのprefetch_relatedの使用例:

from django.shortcuts import render
from .models import Company

def company_list(request):
    # 関連Personデータも一緒に取得
    companies = Company.objects.prefetch_related('persons').all()
    
    return render(request, 'companies/list.html', {
        'companies': companies
    })

def company_detail(request, company_id):
    # 個別データ取得時も関連Personデータを効率的に取得
    company = Company.objects.prefetch_related('persons').get(id=company_id)
    
    return render(request, 'companies/detail.html', {
        'company': company
    })

テンプレートでの使用例(list.html):

<h1>会社一覧</h1>

{% for company in companies %}
    <div class="company-card">
        <h2>{{ company.name }}</h2>
        <h3>社員一覧</h3>
        <ul>
            {% for person in company.persons.all %}
                <li>{{ person.name }}</li>  <!-- 追加SQLなしでアクセス可能 -->
            {% empty %}
                <li>社員はいません</li>
            {% endfor %}
        </ul>
    </div>
{% endfor %}
補足:
  • prefetch_relatedは多対多(ManyToManyField)や1対多(逆参照のForeignKey)の関係で使用します。
  • 多対1の関係には、select_relatedの方が適しています。
  • prefetch_relatedでは、リレーションの名前はモデルで定義された関連名(related_name)か、デフォルトの関連名(モデル名の小文字_set)を使用します。
  • 大量のデータを扱う場合、メモリ使用量に注意が必要です。

まとめ

  • 1側から多側のデータは子モデル名_set.all()で取得できる
  • related_nameを指定すると、カスタム名で逆参照できる
  • prefetch_related()を使うと逆方向のN+1問題を解消できる
  • _setマネージャーはfilter(), count(), exists()などのメソッドが使える
  • テンプレートでも{{ parent.child_set.all }}の形で逆参照が可能