8.

django-filter の lookup_expr 完全リファレンス(DRF + FilterSet 例付き)

編集
この記事の要点
  • django-filter はクエリパラメータから ORM フィルタへの自動変換を行うライブラリ
  • lookup_expr完全一致 / 部分一致 / 大小比較 / IN / NULL 判定 / 正規表現 など切り替え
  • 主要 lookup: exact / iexact / contains / icontains / gt / gte / lt / lte / in / startswith / endswith / range / isnull / regex
  • FilterSet でフィールドごとに lookup を宣言、または filterset_fields = {"name": ["icontains"]} で簡易宣言
  • 日付範囲は DateFromToRangeFilter、複数値は BaseInFilter + カンマ区切り
  • DRF と組み合わせるなら filter_backends = [DjangoFilterBackend]

インストールと最小構成

pip install django-filter
# settings.py
INSTALLED_APPS = [
    ...,
    'django_filters',
    'rest_framework',
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}

lookup_expr 一覧

lookup_exprSQL 相当例(URL)
exact= 'foo'?name=foo
iexact大文字小文字無視 =?name__iexact=FOO
containsLIKE '%foo%'?name__contains=oo
icontains大文字小文字無視 LIKE?name__icontains=oo
startswithLIKE 'foo%'?name__startswith=Ja
istartswith大文字小文字無視?name__istartswith=ja
endswithLIKE '%foo'?name__endswith=son
gt / gte> / >=?price__gte=1000
lt / lte< / <=?price__lt=5000
inIN (1,2,3)?status__in=1,2,3
rangeBETWEEN a AND b?price__range=100,500
isnullIS NULL?deleted_at__isnull=true
regex正規表現?name__regex=^A.*
date / year / month / day日付の部分一致?created__year=2026

FilterSet で明示的に定義

# filters.py
import django_filters
from .models import Product

class ProductFilter(django_filters.FilterSet):
    # 名前: 部分一致
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')

    # 価格: 範囲指定
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')

    # カテゴリ: 完全一致
    category = django_filters.NumberFilter(field_name='category_id', lookup_expr='exact')

    # 複数カテゴリ: IN
    categories = django_filters.BaseInFilter(field_name='category_id', lookup_expr='in')

    # 作成日: 範囲(YYYY-MM-DD,YYYY-MM-DD)
    created = django_filters.DateFromToRangeFilter(field_name='created_at')

    # NULL チェック
    discontinued = django_filters.BooleanFilter(field_name='discontinued_at', lookup_expr='isnull', exclude=True)

    class Meta:
        model = Product
        fields = ['name', 'price_min', 'price_max', 'category', 'categories', 'created', 'discontinued']

ViewSet と組み合わせる

# views.py
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer
from .filters import ProductFilter

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ProductFilter

クエリ例:

# 名前に "ペン" を含み、価格が 100〜500 円
curl 'http://localhost:8000/api/products/?name=ペン&price_min=100&price_max=500'

# カテゴリ 1, 2, 3 のいずれか
curl 'http://localhost:8000/api/products/?categories=1,2,3'

# 2026年1月〜3月作成
curl 'http://localhost:8000/api/products/?created_after=2026-01-01&created_before=2026-03-31'

# 廃版でないもの
curl 'http://localhost:8000/api/products/?discontinued=true'

filterset_fields による簡易宣言

FilterSet クラスを書かず、ViewSet 内で完結:

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]

    # シンプル: フィールド = exact マッチのみ
    # filterset_fields = ['category', 'status']

    # 詳細: フィールドごとに複数 lookup を許可
    filterset_fields = {
        'name': ['exact', 'icontains', 'startswith'],
        'price': ['exact', 'gt', 'lt', 'range'],
        'category': ['exact', 'in'],
        'created_at': ['date', 'year', 'gte', 'lte'],
    }

これで ?name__icontains=ペン, ?price__gt=500, ?created_at__year=2026 のようなクエリが自動で動きます。

カスタムフィルタ(method パラメータ)

単純 lookup では足りない複雑な条件:

from django.db.models import Q

class ProductFilter(django_filters.FilterSet):
    # 名前 or 説明文を横断検索
    search = django_filters.CharFilter(method='filter_search')

    def filter_search(self, queryset, name, value):
        return queryset.filter(
            Q(name__icontains=value) | Q(description__icontains=value)
        )

    # 在庫あり / なし
    in_stock = django_filters.BooleanFilter(method='filter_stock')

    def filter_stock(self, queryset, name, value):
        if value:
            return queryset.filter(stock_count__gt=0)
        return queryset.filter(stock_count=0)

    class Meta:
        model = Product
        fields = ['search', 'in_stock']

ChoiceFilter / TypedChoiceFilter

class OrderFilter(django_filters.FilterSet):
    STATUS_CHOICES = [
        ('pending', '保留中'),
        ('shipped', '発送済'),
        ('delivered', '配達完了'),
    ]
    status = django_filters.ChoiceFilter(choices=STATUS_CHOICES)

    class Meta:
        model = Order
        fields = ['status']

FAQ

Q: lookup_expr と filterset_fields どちらを使う?
A: 単純なら filterset_fields。フィールド名を URL と一致させたい / カスタムロジックが欲しい場合は FilterSet クラス。

Q: 結合テーブルのフィールドでフィルタしたい
A: field_name='category__name' のように __ でリレーション辿れます。

Q: GET だけでなく POST でもフィルタしたい
A: 標準は GET。POST 対応は DRF のカスタムバックエンドを書く。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. MySQL/MariaDBへの接続
  2. sqliteへの接続
  3. SELECT, INSERT, UPDATE, DELETE
  4. 素のSQLを直接実行する方法
  5. Order by DESCの指定方法
  6. limit, offsetの指定方法
  7. filterの検索オプション
  8. django-filterのlookup_expr検索オプション
  9. モデルの内部結合(1対1)