ORM

Django ORMでリレーションデータを効率的に取得|prefetch_relatedの使い方

DjangoのORMで逆方向リレーションや多対多のデータを効率的に取得するには、prefetch_related()を使用します。N+1問題を解消し、データベースへのクエリ回数を最小限に抑えることができます。

基本的な使い方

views.py
from django.db.models import Prefetch

person_prefetch = Prefetch('person', queryset=Person.objects.filter(name='test'))
company = Company.objects.prefetch_related(person_prefetch).get(pk=1)

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

説明

Step 1Prefetchオブジェクトの基本

Djangoでは、Prefetchオブジェクトを使用してprefetch_relatedで取得する関連モデルをさらに細かくフィルタリングすることができます。基本的な構文は以下の通りです:

from django.db.models import Prefetch

変数名 = Prefetch('リレーションのフィールド名', queryset=多側のモデルでのORM)

定義した変数をprefetch_related()の引数にすることで、フィルタリングされたリレーションのデータのみを取得することができます。

Step 2基本的な使用例

例えば、Companyモデルに関連付けられたPersonモデルのうち、特定の条件に一致するものだけを取得する場合:

from django.db.models import Prefetch
from .models import Company, Person

# nameが'test'のPersonだけをプリフェッチする
persons_prefetch = Prefetch('persons', queryset=Person.objects.filter(name='test'))

# Companyモデルのpkが1のデータと、それに関連する特定のPersonデータを取得
company = Company.objects.prefetch_related(persons_prefetch).get(pk=1)

上の例では、Companyモデルのpkが1のモデルに紐づいたPersonの中でnameフィールドがtestのデータだけを取得しています。

Step 3Prefetchの様々な使用例

Prefetchオブジェクトではfilterだけでなく、様々なクエリメソッドを使用できます:

# 並べ替え(order_by)
active_persons = Prefetch('persons', 
                         queryset=Person.objects.filter(is_active=True).order_by('name'))

# 除外(exclude)
non_tokyo_persons = Prefetch('persons', 
                            queryset=Person.objects.exclude(location='東京'))

# 取得フィールドの制限(values)
persons_names = Prefetch('persons', 
                         queryset=Person.objects.values('id', 'name'))

Step 4複数のPrefetchの組み合わせ

複数のPrefetchオブジェクトを組み合わせることもできます:

from django.db.models import Prefetch

# アクティブな社員をプリフェッチ
active_persons = Prefetch('persons', 
                         queryset=Person.objects.filter(is_active=True))

# 東京の部署をプリフェッチ
tokyo_departments = Prefetch('departments', 
                            queryset=Department.objects.filter(location='東京'))

# 両方を適用して会社データを取得
companies = Company.objects.prefetch_related(active_persons, tokyo_departments).all()

Step 5ネストしたPrefetch

Prefetchオブジェクト内でさらにprefetch_relatedを使うことも可能です:

# Personに関連するProjectをフィルタリングしてプリフェッチ
projects_prefetch = Prefetch('projects', 
                            queryset=Project.objects.filter(status='active'))

# Personをプリフェッチし、さらにそのPersonに関連するProjectもプリフェッチ
persons_with_projects = Prefetch('persons', 
                                queryset=Person.objects.prefetch_related(projects_prefetch))

# 会社と、その社員、さらにその社員のプロジェクトを取得
companies = Company.objects.prefetch_related(persons_with_projects).all()

Step 6実践的な使用例

views.pyでのPrefetchオブジェクトの使用例:

from django.shortcuts import render
from django.db.models import Prefetch
from .models import Company, Person, Project

def company_detail(request, company_id):
    # リクエストからフィルターパラメータを取得
    department = request.GET.get('department')
    active_only = request.GET.get('active_only') == 'true'
    
    # 基本的な社員クエリを作成
    persons_query = Person.objects.all()
    
    # フィルターを適用
    if department:
        persons_query = persons_query.filter(department=department)
    
    if active_only:
        persons_query = persons_query.filter(is_active=True)
    
    # 常に名前で並べ替え
    persons_query = persons_query.order_by('name')
    
    # Prefetchオブジェクトを作成
    persons_prefetch = Prefetch('persons', queryset=persons_query)
    
    # プロジェクトもプリフェッチ(アクティブなプロジェクトのみ)
    projects_prefetch = Prefetch('projects', 
                               queryset=Project.objects.filter(status='active'))
    
    # 会社データと関連データを取得
    company = Company.objects.prefetch_related(
        persons_prefetch,
        'departments',
        Prefetch('persons__projects', queryset=Project.objects.filter(status='active'))
    ).get(id=company_id)
    
    return render(request, 'companies/detail.html', {
        'company': company,
        'department_filter': department,
        'active_only': active_only
    })

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

<h1>{{ company.name }}の詳細</h1>

<h2>社員一覧</h2>
<div class="filters">
    <form method="get">
        <select name="department">
            <option value="">すべての部門</option>
            {% for dept in company.departments.all %}
                <option value="{{ dept.name }}" {% if department_filter == dept.name %}selected{% endif %}>
                    {{ dept.name }}
                </option>
            {% endfor %}
        </select>
        
        <label>
            <input type="checkbox" name="active_only" value="true" {% if active_only %}checked{% endif %}>
            アクティブな社員のみ表示
        </label>
        
        <button type="submit">フィルター適用</button>
    </form>
</div>

<ul class="employees">
    {% for person in company.persons.all %}
        <li>
            <h3>{{ person.name }} ({{ person.department }})</h3>
            
            {% if person.projects.exists %}
                <h4>担当プロジェクト:</h4>
                <ul>
                    {% for project in person.projects.all %}
                        <li>{{ project.name }} ({{ project.status }})</li>
                    {% endfor %}
                </ul>
            {% else %}
                <p>担当プロジェクトはありません</p>
            {% endif %}
        </li>
    {% empty %}
        <li>条件に一致する社員はいません</li>
    {% endfor %}
</ul>
重要ポイント:
  • Prefetchの第1引数は「リレーションのフィールド名」、querysetパラメータには「多側のモデルをどのようにするか」を指定します。
  • Prefetchオブジェクトを使うことで、関連データの取得をより細かく制御でき、必要なデータだけを効率的に取得できます。
  • 複雑なフィルタリングやソートが必要な場合、Prefetchオブジェクトは特に有用です。
  • ネストした関連(例:company→person→project)でも使用できますが、クエリが複雑になるため注意が必要です。

まとめ

  • prefetch_related()は逆方向リレーションや多対多のデータを効率的に取得する
  • 追加のSQLクエリで関連データを一括取得し、Python側でマッピングする
  • Prefetchオブジェクトを使うとフィルタリングや並び替えを指定できる
  • select_related()はJOINで取得する(ForeignKey向け)、prefetch_related()は別クエリで取得する
  • N+1問題の解消に効果的で、パフォーマンス改善の基本テクニック