ORM

Django ORMの関連フィルタ入門|紐づいたモデルの条件検索

Django ORMで、ForeignKeyやManyToManyで紐づいたモデルのフィールドを条件にしてデータをフィルタリングする方法を解説します。ダブルアンダースコア(__)記法を使うと、関連モデルのフィールドを簡単に参照できます。

基本的な使い方

関連モデルのフィールドでフィルタするには、関連名__フィールド名の形式で指定します。

views.py
from .models import Company, Employee

def index(request):
    # Employeeのnameが"test"であるCompanyを取得
    companies = Company.objects.filter(
        employee__name="test"
    )
    print(companies)
実行結果
<QuerySet [<Company: 株式会社A>]>

employee__nameのように、関連モデル名(小文字)にダブルアンダースコアを付けてフィールド名を指定します。

複数条件でのフィルタリング

関連モデルの複数のフィールドを条件に指定することもできます。

views.py
# Employeeのidが1〜10かつnameが"test"であるCompanyを取得
companies = Company.objects.filter(
    employee__id__range=(1, 10),
    employee__name="test"
)

# 自身の条件と関連モデルの条件を組み合わせる
companies = Company.objects.filter(
    id=1,
    employee__name="test"
)

filter()に複数の条件を渡すと、全てAND条件で結合されます。

select_relatedで効率的に取得

関連モデルのデータもテンプレートで使う場合は、select_related()を使ってN+1問題を回避します。

views.py
# N+1問題を防ぎつつ、関連データでフィルタリング
employees = Employee.objects.select_related('company').filter(
    company__name="株式会社A"
)

# 各employeeのcompany.nameにアクセスしても追加クエリが発生しない
for emp in employees:
    print(f"{emp.name} - {emp.company.name}")
実行結果
山田太郎 - 株式会社A
佐藤花子 - 株式会社A

prefetch_relatedで逆方向の関連を取得

1対多の「1」側から「多」側を取得する場合はprefetch_related()を使います。

views.py
from django.db.models import Prefetch

# 基本的な使い方
companies = Company.objects.prefetch_related('employee_set').filter(
    employee__name__contains="田"
).distinct()

# Prefetchオブジェクトで関連データ自体にもフィルタをかける
companies = Company.objects.prefetch_related(
    Prefetch(
        'employee_set',
        queryset=Employee.objects.filter(age__gte=20),
        to_attr='adult_employees'
    )
)

Prefetchオブジェクトを使うと、関連データ自体にもフィルタ条件を適用できます。

ルックアップの種類

関連モデルのフィールドに対しても、通常のルックアップが全て使えます。

views.py
# 部分一致
Company.objects.filter(employee__name__contains="田中")

# 前方一致
Company.objects.filter(employee__name__startswith="山")

# 範囲指定
Company.objects.filter(employee__age__range=(20, 30))

# NULLチェック
Company.objects.filter(employee__email__isnull=False)

# リスト内の値に一致
Company.objects.filter(employee__department__in=["営業部", "技術部"])

実践的な使用例

ビューでの実践的な検索機能の例です。

views.py
from django.shortcuts import render
from .models import Company, Employee

def company_search(request):
    companies = Company.objects.prefetch_related('employee_set')

    # 社員名で会社を検索
    employee_name = request.GET.get('employee_name')
    if employee_name:
        companies = companies.filter(
            employee__name__contains=employee_name
        ).distinct()

    # 部署で絞り込み
    department = request.GET.get('department')
    if department:
        companies = companies.filter(
            employee__department=department
        ).distinct()

    return render(request, 'companies/search.html', {
        'companies': companies
    })
ポイント

関連モデルでフィルタすると結果にJOINが使われるため、重複が発生することがあります。.distinct()を付けて重複を排除しましょう。

注意

select_related()はForeignKeyとOneToOneFieldに対して使います。ManyToManyFieldや逆参照の場合はprefetch_related()を使ってください。間違えるとパフォーマンスが悪化します。

まとめ

  • 関連名__フィールド名のダブルアンダースコア記法で関連モデルの条件を指定できる
  • 複数条件をfilter()に渡すとAND条件になる
  • select_related()でForeignKey先のデータを効率的に取得できる
  • prefetch_related()で逆参照やManyToManyのデータを取得できる
  • 関連モデルでフィルタ時は.distinct()で重複を排除する