Djangoで複雑な検索フォームを簡単に実装!django-filter徹底ガイド
はじめに:Djangoでの検索フォーム実装の課題とdjango-filter
の力
Webアプリケーション開発において、ユーザーが膨大なデータの中から目的の情報を見つけ出すための「検索機能」は非常に重要な要素です。特に、ユーザー数やデータ量が増えるにつれて、シンプルさだけでなく、複数の条件を組み合わせたり、特定の範囲で絞り込んだりできる「複雑な検索フォーム」が求められるようになります。
Djangoフレームワークで検索機能を実装する場合、最も基本的な方法は、GETリクエストで受け取った検索キーワードを元に、ビュー関数やクラス内でQuerySet
に対してfilter()
メソッドを呼び出すことです。例えば、商品リストの中から名前に特定のキーワードが含まれる商品を検索する場合、ビュー関数内で以下のようなコードを書くことになるでしょう。
“`python
views.py
from django.shortcuts import render
from .models import Product
def product_list(request):
products = Product.objects.all()
query = request.GET.get(‘q’)
if query:
products = products.filter(name__icontains=query)
return render(request, ‘products/product_list.html’, {‘products’: products})
“`
これは非常にシンプルで分かりやすい方法です。しかし、検索条件が一つ増えるだけでも、ビューのコードは少し複雑になります。例えば、価格帯での絞り込みも追加する場合:
“`python
views.py
def product_list(request):
products = Product.objects.all()
query = request.GET.get(‘q’)
min_price = request.GET.get(‘min_price’)
max_price = request.GET.get(‘max_price’)
if query:
products = products.filter(name__icontains=query)
if min_price:
try:
products = products.filter(price__gte=float(min_price))
except ValueError:
pass # 価格が無効な場合は無視
if max_price:
try:
products = products.filter(price__lte=float(max_price))
except ValueError:
pass # 価格が無効な場合は無視
return render(request, 'products/product_list.html', {'products': products})
“`
さらに、カテゴリでの絞り込み、在庫状況、登録日、関連ユーザーなど、検索条件が増え、それぞれの条件でAND/OR検索、範囲指定、前方一致/後方一致、特定の値との比較といった多様なフィルタリングが必要になると、ビューのコードは指数関数的に複雑化していきます。
- GETパラメータの取得とバリデーション
- 各パラメータに応じた
QuerySet
のフィルタリング処理 - 複数のフィルタリング条件の組み合わせ(通常はAND)
- フォームの表示(入力フィールド、選択肢など)
これらの処理を手動で記述していくのは、非常に手間がかかるだけでなく、コードの見通しが悪くなり、バグの温床となりがちです。特に、フォームの生成とQuerySet
のフィルタリング処理を同期させるのは骨が折れる作業です。
そこで登場するのが、django-filter
ライブラリです。django-filter
は、Djangoモデルに基づいた強力で柔軟なフィルタリングシステムを提供します。モデルフィールドに対応するフォームフィールドを自動生成し、ユーザーからのGETリクエストパラメータを解釈して、対応するQuerySet
フィルタリングを適用してくれます。これにより、開発者はフィルタリングロジックとフォーム表示の面倒な部分から解放され、宣言的に検索機能を定義できるようになります。
この記事では、django-filter
の基本的な使い方から、さまざまなフィルタリングタイプ、フォームのカスタマイズ、ビューとの連携、応用的な使い方、パフォーマンスに関する考慮事項、テスト方法、そして他の選択肢との比較まで、django-filter
を使いこなすためのすべてを徹底的に解説します。
対象読者は、Djangoの基本的な使い方を理解しており、アプリケーションに検索機能や一覧表示の絞り込み機能を追加したいと考えている方です。この記事を読めば、複雑な検索フォームの実装が驚くほど簡単になることを実感できるでしょう。
さあ、django-filter
の世界に飛び込み、洗練された検索機能を実装しましょう!
django-filter
の基本:インストールとシンプルな使い方
まずは、django-filter
をプロジェクトに導入し、最も基本的なフィルタリングを実装してみましょう。
インストール
django-filter
はpipを使って簡単にインストールできます。
bash
pip install django-filter
インストール後、Djangoプロジェクトのsettings.py
にdjango_filter
をINSTALLED_APPS
に追加します。
“`python
settings.py
INSTALLED_APPS = [
# … 既存のアプリ …
‘django_filter’,
# …
]
“`
これでdjango-filter
を使う準備が整いました。
サンプルモデルの準備
フィルタリング対象となるシンプルなDjangoモデルを定義します。ここでは、商品のリストをフィルタリングすることを考えます。
“`python
products/models.py
from django.db import models
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField(default=0)
is_available = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name=’products’)
def __str__(self):
return self.name
“`
マイグレーションを実行してデータベースにテーブルを作成しておきましょう。
bash
python manage.py makemigrations
python manage.py migrate
管理サイトやスクリプトを使って、いくつかのテストデータを登録しておくと、動作確認がしやすくなります。
FilterSet
クラスの定義
django-filter
の中心となるのがFilterSet
クラスです。これは、どのモデルに対してどのようなフィルタリングを適用するかを定義するクラスです。ModelForm
と似たような宣言的な構文を使います。
products
アプリ内にfilters.py
ファイルを作成し、以下のように記述します。
“`python
products/filters.py
import django_filters
from .models import Product
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = [‘name’, ‘price’, ‘is_available’, ‘category’]
“`
このコードは以下のことを指定しています。
ProductFilter
はdjango_filters.FilterSet
を継承しています。Meta
クラス内で、このフィルタセットが対象とするモデルをmodel = Product
で指定しています。fields = [...]
リストで、フィルタリングを有効にしたいモデルフィールドを指定しています。
django-filter
は、fields
に指定されたモデルフィールドのタイプを見て、適切なデフォルトのフィルタリングタイプとフォームフィールドを自動的に選択します。
name
(CharField) ->CharFilter
(デフォルトはexact
またはiexact
)price
(DecimalField) ->NumberFilter
(デフォルトはexact
)is_available
(BooleanField) ->BooleanFilter
category
(ForeignKey) ->ModelChoiceFilter
ビューでの利用
次に、定義したProductFilter
をビューで使用します。ジェネリックビュー(ListView
など)と組み合わせるのが一般的ですが、ここではまずシンプルな関数ベースビューで使い方を見てみましょう。
“`python
products/views.py
from django.shortcuts import render
from .models import Product
from .filters import ProductFilter # 作成したFilterSetをインポート
def product_list(request):
# FilterSetのインスタンスを作成
# request.GET をデータとして渡し、フィルタリング対象のQuerySet (Product.objects.all()) を指定
filter = ProductFilter(request.GET, queryset=Product.objects.all())
# filter.qs がフィルタリング適用後のQuerySetになります
context = {
'filter': filter,
'product_list': filter.qs # フィルタリングされた商品リスト
}
return render(request, 'products/product_list.html', context)
“`
ビュー関数内でProductFilter
のインスタンスを作成しています。コンストラクタには、ユーザーからのフィルタリング条件を含む辞書(通常はrequest.GET
)と、フィルタリング対象となる初期のQuerySet
(ここではProduct.objects.all()
)を渡します。
filter
インスタンスの.qs
属性に、フィルタリングが適用された後のQuerySet
が格納されています。これをテンプレートに渡して表示すれば、フィルタリングされたリストが表示されます。
テンプレートでの表示
フィルタリングフォームと結果を表示するためのテンプレートを作成します。
“`html
{# products/templates/products/product_list.html #}
商品リスト
検索/絞り込み
{# フィルタリングフォームを表示。メソッドはGETが一般的 #}
検索結果
{# filter.qs にフィルタリングされた商品リストが入っています #}
{% if product_list %}
名前 | 価格 | 在庫 | 公開中 | 作成日 | カテゴリ |
---|---|---|---|---|---|
{{ product.name }} | {{ product.price }} | {{ product.stock }} | {{ product.is_available }} | {{ product.created_at|date:”Y-m-d” }} | {{ product.category|default:”未分類” }} |
{% else %}
条件に一致する商品はありません。
{% endif %}
“`
テンプレートでは、filter.form
を使ってフィルタリングフォームを表示し、filter.qs
を使ってフィルタリング結果のリストを表示しています。ユーザーがフォームを送信すると、GETパラメータがビューに渡され、ProductFilter(request.GET, ...)
によってそのパラメータが解釈され、適切なフィルタリングが行われます。
URLconfの設定
最後に、作成したビューにアクセスするためのURLを設定します。
“`python
products/urls.py
from django.urls import path
from . import views
urlpatterns = [
path(‘products/’, views.product_list, name=’product_list’),
]
“`
プロジェクトのルートurls.py
からこのURLconfをインクルードするのを忘れないでください。
“`python
your_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path(‘admin/’, admin.site.urls),
path(”, include(‘products.urls’)), # productsアプリのURLconfをインクルード
]
“`
これで、/products/
にアクセスすると、商品リストが表示され、フォームを使って名前、価格、公開状況、カテゴリでフィルタリングできるようになりました。
試してみましょう:
- ブラウザで
/products/
にアクセスします。全商品が表示されるはずです。 - フォームで「名前」にキーワード(例: “Test”)を入力して「検索」をクリックします。名前に”Test”を含む商品だけが表示されます。
- フォームで「価格」に特定の価格(例: “100”)を入力して「検索」をクリックします。価格が100の商品だけが表示されます。
- 「公開中」のチェックボックスをオンにして「検索」をクリックします。公開中の商品だけが表示されます。
- 「カテゴリ」で特定のカテゴリを選択して「検索」をクリックします。そのカテゴリに属する商品だけが表示されます。
- 複数の条件を組み合わせて検索することも可能です。例えば、「名前」にキーワード、「価格」に特定の値を入力して検索すると、両方の条件を満たす商品が表示されます。
このように、わずかなコードで強力なフィルタリング機能が実装できました。これがdjango-filter
の最も基本的な使い方です。
さまざまなフィルタータイプとオプション
django-filter
は、モデルフィールドタイプに基づいてデフォルトのフィルタータイプを選択してくれますが、開発者が明示的にフィルタータイプを指定したり、詳細なオプションを設定したりすることも可能です。これにより、より複雑なフィルタリング要件に対応できます。
明示的なフィルター定義
FilterSet
クラス内で、Meta
クラスのfields
リストを使う代わりに、Djangoのフォームフィールドのように各フィルターフィールドを明示的に定義できます。これにより、フィルタータイプやオプションを細かく制御できます。
“`python
products/filters.py
import django_filters
from .models import Product, Category
from django.forms import TextInput # ウィジェットの指定に使う場合
class ProductFilter(django_filters.FilterSet):
# 名前で部分一致検索 (大文字小文字を区別しない)
name = django_filters.CharFilter(lookup_expr=’icontains’,
label=’商品名’,
widget=TextInput(attrs={‘placeholder’: ‘キーワードを入力’}))
# 価格で範囲検索
# lookup_exprをデフォルト('exact')から変更
price__gte = django_filters.NumberFilter(lookup_expr='gte', label='価格 (以上)')
price__lte = django_filters.NumberFilter(lookup_expr='lte', label='価格 (以下)')
# 公開状況 (BooleanFilterはデフォルトで適切なフォームフィールドを生成)
is_available = django_filters.BooleanFilter(label='公開中のみ')
# カテゴリ (ForeignKeyに対応するModelChoiceFilter)
# ModelChoiceFilterはデフォルトで__exact検索だが、__inなども指定可能
category = django_filters.ModelChoiceFilter(queryset=Category.objects.all(),
label='カテゴリ',
empty_label='全てのカテゴリ') # 選択肢の先頭に追加
# 作成日で範囲検索 (DateFilterを使う)
# デフォルトのlookup_exprは'exact'だが、範囲検索のためにgte/lteを指定
created_at__date__gte = django_filters.DateFilter(lookup_expr='date__gte', label='作成日 (以降)')
created_at__date__lte = django_filters.DateFilter(lookup_expr='date__lte', label='作成日 (以前)')
class Meta:
model = Product
# fields を指定しない場合、明示的に定義したフィルターのみが使用される
# fields = ['name', 'price', 'is_available', 'category'] # この行は削除またはコメントアウト
# exclude = ['description'] # 除外したいフィールドを指定することもできる(fieldsと排他的)
# 複数フィールドをまとめて定義することもできるが、個別に定義した方が柔軟性は高い
# fields = {
# 'name': ['icontains'],
# 'price': ['gte', 'lte'],
# 'is_available': ['exact'],
# 'category': ['exact'],
# 'created_at': ['date__gte', 'date__lte'], # 'date'は日付部分を取り出すためのルックアップ
# }
# このMeta.fieldsの辞書形式で指定する方法でも、上記の明示的な定義と同じ効果が得られます。
# 個別にカスタマイズが多い場合は明示的な定義、シンプルな場合は辞書形式が良いでしょう。
“`
この例では、以下の点を変更・追加しています。
name
:CharFilter
を明示的に定義し、lookup_expr='icontains'
を指定することで、大文字小文字を区別しない部分一致検索を実現しています。label
でフォームフィールドのラベルを設定し、widget
でHTMLのウィジェットをカスタマイズしています。price__gte
,price__lte
:NumberFilter
を使い、lookup_expr
にgte
(以上)とlte
(以下)を指定することで、価格の範囲検索を可能にしています。フィールド名に__gte
や__lte
のようなサフィックスを付けるのは慣習的なものですが、django-filter
はこの命名規則を自動的に解釈するわけではありません。個別に定義する場合は、フィールド名自体は任意ですが、フィルタリング対象のモデルフィールド(ここではprice
)を指定するために、lookup_expr
を使います。is_available
:BooleanFilter
は特にオプションを指定していませんが、ラベルを設定しています。category
:ModelChoiceFilter
を使い、queryset
とlabel
、そしてempty_label
(「全てのカテゴリ」という選択肢)を指定しています。created_at__date__gte
,created_at__date__lte
:DateFilter
を使い、日付の範囲検索を可能にしています。lookup_expr='date__gte'
のように、フィールドタイプ(DateTimeField)から日付部分だけを取り出すためのdate
ルックアップと、比較演算子gte
を組み合わせて指定しています。
このように、フィルターフィールドを明示的に定義することで、lookup_expr
、label
、widget
、queryset
(ModelChoiceFilter
の場合)など、さまざまなオプションを細かく制御できます。
主要なフィルタータイプとlookup_expr
django-filter
が提供する主要なフィルタータイプと、それらに対応するDjangoのQuerySet
ルックアップ (lookup_expr
) をいくつか紹介します。
Filter Type | 対象モデルフィールドタイプ | デフォルト lookup_expr |
主なオプション | よく使われる lookup_expr |
説明 |
---|---|---|---|---|---|
CharFilter |
CharField , TextField , SlugField , etc. |
exact or iexact |
lookup_expr , label , widget , strip |
exact , iexact , contains , icontains , startswith , istartswith , endswith , iendswith , regex , iregex |
テキストフィールドの検索 |
NumberFilter |
IntegerField , FloatField , DecimalField , etc. |
exact |
lookup_expr , label , widget |
exact , gt , lt , gte , lte |
数値フィールドの検索 |
RangeFilter |
NumberField , DateField , DateTimeField , etc. |
N/A | lookup_expr , label , widget |
内部で2つのフィールド(field_name_min , field_name_max のような)を生成し、範囲検索を行う |
特定のフィールドの最小値〜最大値で検索(例: 価格帯、日付範囲) |
DateFilter |
DateField |
exact |
lookup_expr , label , widget , input_formats |
exact , gt , lt , gte , lte , year , month , day , week , week_day , quarter |
日付フィールドの検索 |
DateTimeFilter |
DateTimeField |
exact |
lookup_expr , label , widget , input_formats |
exact , gt , lt , gte , lte , date , time , year , etc. |
日時フィールドの検索 |
BooleanFilter |
BooleanField |
exact |
label , widget |
exact |
真偽値フィールドの検索(通常はチェックボックスやドロップダウンリスト) |
ChoiceFilter |
固定された選択肢リストに基づく | exact |
choices , lookup_expr , label , widget |
exact , in |
特定の値の中から選択してフィルタリング(モデルフィールドに依存しないことも可能) |
ModelChoiceFilter |
ForeignKey |
exact |
queryset , lookup_expr , label , widget , empty_label |
exact , in , isnull |
関連モデルオブジェクトを選択してフィルタリング |
ModelMultipleChoiceFilter |
ManyToManyField |
exact or in |
queryset , lookup_expr , label , widget |
exact , in , isnull |
複数の関連モデルオブジェクトを選択してフィルタリング |
MethodFilter |
モデルフィールドに直接対応しない | N/A | action , label , widget , field_name |
なし (カスタムメソッドを呼び出す) | 複雑なカスタムフィルタリングロジックを実装 |
RangeFilterの例:
price
フィールドに対してRangeFilter
を使うと、価格の最小値と最大値を入力するフォームフィールドを自動生成できます。
“`python
products/filters.py (ProductFilterクラス内)
import django_filters
… 他のインポート …
class ProductFilter(django_filters.FilterSet):
# … 他のフィルター定義 …
price = django_filters.RangeFilter(label='価格帯')
class Meta:
model = Product
# fields = ['name', 'price', 'is_available', 'category'] # この場合、Meta.fieldsでもRangeFilterが自動生成される
fields = ['name', 'is_available', 'category', 'price'] # Meta.fieldsで指定
# または、明示的に定義したフィルターのみを使う場合はfields/excludeを省略
# fields = {
# 'name': ['icontains'],
# 'is_available': ['exact'],
# 'category': ['exact'],
# }
# priceは個別にRangeFilterとして定義済み
“`
RangeFilter
を使用する場合、フォームにはデフォルトで「フィールド名_min」と「フィールド名_max」のような名前の2つの入力フィールドが表示されます。django-filter
はこれらの値を自動的に解釈し、queryset.filter(price__gte=min_value, price__lte=max_value)
のようなクエリを生成します。
MethodFilterの例:
複数のフィールドをまとめて検索したい場合や、複雑な条件でフィルタリングしたい場合はMethodFilter
が便利です。例えば、「名前」または「説明」にキーワードが含まれる商品を検索する場合を考えます。
“`python
products/filters.py (ProductFilterクラス内)
import django_filters
from django.db.models import Q
… 他のインポート …
class ProductFilter(django_filters.FilterSet):
# 検索キーワード (MethodFilterを使用)
# action引数にフィルタリング処理を行うメソッド名を指定
# field_nameはオプションだが、このフィルターがどのモデルフィールドに対応するかを示すために設定することも
search_query = django_filters.MethodFilter(action=’filter_by_search_query’, label=’キーワード検索’)
# ... 他のフィルター定義 (名前、説明などの個別のフィルターは不要になる場合も) ...
class Meta:
model = Product
# search_query 以外に、例えば価格帯やカテゴリのフィルターを残す場合
fields = ['search_query', 'price', 'is_available', 'category']
# MethodFilterで指定したメソッド
# query_set: フィルタリング対象の現在のQuerySet
# name: フィルターフィールドの名前 ('search_query')
# value: ユーザーが入力した値 (検索キーワード)
def filter_by_search_query(self, queryset, name, value):
if not value:
return queryset # 値がなければQuerySetを変更せず返す
# Qオブジェクトを使って、名前または説明にキーワードが含まれる条件をOR結合
# icontainsは大文字小文字を区別しない部分一致
return queryset.filter(
Q(name__icontains=value) | Q(description__icontains=value)
)
“`
MethodFilter
を使うと、filter_by_search_query
のようなカスタムメソッドを定義できます。このメソッドは現在のQuerySet
を受け取り、カスタムロジックでフィルタリングした新しいQuerySet
を返します。これにより、DjangoのQ
オブジェクトを使った複雑な条件や、データベース以外の情報に基づいたフィルタリングなど、柔軟な処理が可能になります。
lookup_exprの指定方法
lookup_expr
は、django-filter
がユーザーの入力値とモデルフィールドに対してどのようなデータベース比較を行うかを指定します。これはDjangoのQuerySet
のfilter()
メソッドで使用されるルックアップと同じものです(例: name__icontains
, price__gte
)。
lookup_expr
は、以下のいずれかの方法で指定できます。
-
Meta.fields
で辞書形式を使う:
python
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = {
'name': ['exact', 'icontains'], # name__exact と name__icontains の2つのフィルターを生成
'price': ['lt', 'gte'], # price__lt と price__gte の2つのフィルターを生成
'created_at': ['year'], # created_at__year のフィルターを生成
}
このようにリストで複数のルックアップを指定すると、それぞれのルックアップに対応する複数のフィルターフィールドが自動生成されます。フィールド名には__{lookup_expr}
サフィックスが付きます(例:name__icontains
,price__lt
)。 -
フィルターフィールドを明示的に定義し、
lookup_expr
引数を渡す:
“`python
class ProductFilter(django_filters.FilterSet):
name_contains = django_filters.CharFilter(field_name=’name’, lookup_expr=’icontains’, label=’名前 (部分一致)’)
price_min = django_filters.NumberFilter(field_name=’price’, lookup_expr=’gte’, label=’価格 (以上)’)
price_max = django_filters.NumberFilter(field_name=’price’, lookup_expr=’lte’, label=’価格 (以下)’)class Meta: model = Product # 明示的に定義したフィルターのみを使うため、fields/excludeは省略または空リスト # fields = []
``
name_contains
この方法では、フィルターフィールドの名前は任意に設定できます(例:,
price_min)。
field_name引数でフィルタリング対象のモデルフィールドを指定し、
lookup_expr`でルックアップを指定します。
どちらの方法を使うかは、生成したいフィルターフィールドの数やカスタマイズの度合いによります。シンプルな複数のルックアップが必要な場合はMeta.fields
の辞書形式が簡潔ですが、各フィルターを細かく制御したい場合は明示的な定義が適しています。
関連モデルでのフィルタリング
ForeignKeyやManyToManyFieldで関連付けられたモデルのフィールドでフィルタリングすることもよくあります。これはDjangoのQuerySetでリレーションシップを辿るのと同じように、__
(ダブルアンダースコア)を使って実現できます。
例えば、Product
モデルがCategory
モデルにForeignKeyで関連付けられている場合、カテゴリ名で商品をフィルタリングしたいとします。
“`python
products/filters.py (ProductFilterクラス内)
import django_filters
… 他のインポート …
class ProductFilter(django_filters.FilterSet):
# カテゴリ名で部分一致検索
# リレーションシップを辿る: category__name
# lookup_expr=’icontains’ で部分一致
category_name = django_filters.CharFilter(field_name=’category__name’,
lookup_expr=’icontains’,
label=’カテゴリ名’)
# 特定のカテゴリIDでフィルタリング(ModelChoiceFilterと同じだが、CharFilterで表現)
# category__id は ForeignKey の ID フィールドを指す
category_id = django_filters.NumberFilter(field_name='category__id',
lookup_expr='exact',
label='カテゴリID')
# カテゴリが指定されているか否かでフィルタリング (__isnullを使う)
# category__isnull=True でカテゴリ未指定の商品、Falseでカテゴリ指定済みの商品を検索
category_isnull = django_filters.BooleanFilter(field_name='category',
lookup_expr='isnull',
label='カテゴリ指定なし')
class Meta:
model = Product
fields = ['name', 'price', 'is_available', 'category_name', 'category_id', 'category_isnull']
# category (ModelChoiceFilter) も残しておくと、カテゴリ名での検索と選択肢からの選択の両方が可能になる
# fields = ['name', 'price', 'is_available', 'category', 'category_name', 'category_id', 'category_isnull']
“`
関連モデルのフィールドを指定するには、field_name
にリレーションシップフィールド名__関連モデルフィールド名
のように記述します。ManyToManyフィールドの場合も同様にManyToManyField名__関連モデルフィールド名
と指定できます。
これにより、複雑なリレーションシップを持つモデル構造でも、簡単にフィルタリング機能を構築できます。
フィルタリングフォームのカスタマイズ
django-filter
はFilterSet
からDjangoのForm
クラスを自動生成します。このフォームはfilter.form
属性としてアクセスできます。フォームの外観や挙動をカスタマイズするには、標準的なDjangoのフォームカスタマイズ手法を利用できます。
ウィジェットの変更
デフォルトで生成されるフォームウィジェットは、フィルタータイプやモデルフィールドタイプに基づいて決定されます(例: CharFilter
はTextInput
、BooleanFilter
はCheckboxInput
、ModelChoiceFilter
はSelect
)。これを変更したい場合は、フィルター定義時にwidget
引数を指定します。
“`python
products/filters.py (ProductFilterクラス内)
import django_filters
from django.forms import Select, CheckboxInput # 使うウィジェットをインポート
… 他のインポート …
class ProductFilter(django_filters.FilterSet):
# … 他のフィルター定義 …
# is_available をドロップダウンリストで選択させる (デフォルトはチェックボックス)
is_available = django_filters.BooleanFilter(
label='公開状況',
widget=Select(choices=[('', 'すべて'), (True, '公開中'), (False, '非公開')]),
# BooleanFilterでSelectウィジェットを使う場合、filter_param_nameなどのオプションで
# パラメータ名を指定しないと、デフォルトの'is_available'パラメータがTrue/False以外の値を受け付けず
# 期待通りに動作しないことがあります。
# または、ChoiceFilterとして定義し直す方がシンプルかもしれません。
)
# price を隠しフィールドにして、JavaScriptなどで値をセットする場合 (例: スライダーUIなど)
# price = django_filters.RangeFilter(label='価格帯', widget=django_filters.widgets.RangeWidget(attrs={'data-range-slider': True})) # 例: カスタムウィジェット
# ModelChoiceFilterのウィジェットをRadioSelectに変更
category = django_filters.ModelChoiceFilter(
queryset=Category.objects.all(),
label='カテゴリ',
empty_label='全てのカテゴリ',
widget=django_filters.widgets.LinkWidget # 例: リンクのリストとして表示
# widget=django.forms.RadioSelect # ラジオボタンとして表示
)
class Meta:
model = Product
fields = ['name', 'price', 'is_available', 'category']
# 明示的に定義したフィルターは Meta.fields に含まれている必要はありません。
# 含まれていない場合は、明示的に定義したフィルターのみが使われます。
# fields = ['name', 'price', 'category'] # is_available は明示的に定義したので省略
“`
django-filter
は、いくつかの便利なウィジェットを提供しています。例えば、RangeWidget
はRangeFilter
用のデフォルトウィジェットですが、カスタム属性などを追加して使用できます。LinkWidget
は、選択肢をHTMLの<a>
タグのリストとして表示するのに便利です。
フォームフィールドのカスタマイズ
ラベルやヘルプテキストなどのフォームフィールド属性は、フィルター定義時の引数で設定できます。
“`python
products/filters.py (ProductFilterクラス内)
import django_filters
… 他のインポート …
class ProductFilter(django_filters.FilterSet):
name = django_filters.CharFilter(
lookup_expr=’icontains’,
label=’商品名’, # ラベルを設定
help_text=’商品名で部分一致検索を行います’, # ヘルプテキストを設定
widget=django.forms.TextInput(attrs={‘class’: ‘form-control’}) # HTML属性を設定
)
# ... 他のフィルター定義 ...
“`
label
やhelp_text
は、フォームフィールドに表示されるテキストをカスタマイズします。widget
のattrs
引数を使うと、HTML要素に任意の属性(例: class
, placeholder
, data-*
属性)を追加できます。これは、CSSフレームワーク(Bootstrapなど)と連携してフォームのスタイルを調整する際に非常に便利です。
フォームクラス自体のカスタマイズ
FilterSet
は内部的にDjangoのForm
クラスを生成しています。この生成されるフォームクラス自体をカスタマイズすることも可能です。FilterSet
のMeta
クラス内にform
属性でカスタムフォームクラスを指定します。
“`python
products/forms.py (新しく作成)
from django import forms
class ProductFilterForm(forms.Form):
# FilterSetが自動生成するフィールドに加えて、
# 独自のフィールドを追加することも可能ですが、
# django-filterが自動でフィルタリングロジックを提供するのは
# FilterSetで定義されたフィルターフィールドのみです。
# このカスタムフォームクラスは、主にMeta.formを使って、
# 生成されるフォームクラス全体に影響を与えるために使います。
# 例: __init__メソッドのオーバーライド、クリーンメソッドの追加など
pass
products/filters.py
import django_filters
… 他のインポート …
from .forms import ProductFilterForm # 作成したフォームクラスをインポート
class ProductFilter(django_filters.FilterSet):
# … フィルター定義 …
class Meta:
model = Product
fields = ['name', 'price', 'is_available', 'category']
form = ProductFilterForm # カスタムフォームクラスを指定
“`
Meta.form
でカスタムフォームクラスを指定した場合、django-filter
はそのカスタムクラスを継承してフォームを生成します。これにより、フォームの__init__
メソッドをオーバーライドしてフィールドを動的に変更したり、カスタムのバリデーション(clean_...
メソッド)を追加したりといった高度なカスタマイズが可能になります。
テンプレートでのフォーム表示
テンプレートでfilter.form
を表示する方法はいくつかあります。
{{ filter.form.as_p }}
: 各フィールドを<p>
タグで囲んで表示します。シンプルですが、カスタマイズ性は低いです。{{ filter.form.as_ul }}
: 各フィールドを<li>
タグで囲んで表示します。{{ filter.form.as_table }}
: 各フィールドを<tr>
タグと<td>
タグを使ってテーブル形式で表示します。- 手動レンダリング: 各フィールドを個別に指定して、HTMLを完全に制御する方法です。最も柔軟性が高い方法です。
“`html
{# products/templates/products/product_list.html (フォーム部分を修正) #}
“`
手動レンダリングを行うことで、フォームフィールドを任意のHTML構造内に配置し、CSSで自由にスタイリングできるようになります。各フィールドはfilter.form.フィールド名
としてアクセスでき、.label_tag
, .widget
, .help_text
, .errors
などの属性を持っています。
フォームの各フィールドをループ処理して表示することも可能です。
“`html
{# products/templates/products/product_list.html (フォーム部分を修正) #}
“`
このループ処理は、シンプルなフォームや、各フィールドを似たようなスタイルで表示したい場合に便利です。必要に応じて、特定のフィールドだけ個別にレンダリングし、残りをループで処理するといった組み合わせも可能です。
これらのカスタマイズ手法を組み合わせることで、デザイン要件に合わせた柔軟なフィルタリングフォームを構築できます。
ビューとの連携強化:FilterView
とジェネリックビュー
django-filter
は、標準のDjangoビューと組み合わせて使うのが一般的ですが、特にジェネリックビューとの連携はスムーズです。また、django-filter
自身が提供するFilterView
クラスを利用すると、さらに簡単にフィルタリング機能を備えたリストビューを実装できます。
ジェネリックビュー(ListView)との組み合わせ
DjangoのListView
は、モデルのリストを表示するのに最適です。django-filter
をListView
と組み合わせることで、フィルタリングされたリストを効率的に表示できます。
“`python
products/views.py
… 他のインポート …
from django.views.generic import ListView
from .models import Product
from .filters import ProductFilter
class ProductListView(ListView):
model = Product # 表示対象のモデル
template_name = ‘products/product_list.html’ # 使用するテンプレート
context_object_name = ‘product_list’ # テンプレートに渡すQuerySetの名前
# ページネーションを設定する場合
# paginate_by = 10
def get_queryset(self):
# デフォルトのQuerySet (Product.objects.all()) を取得
queryset = super().get_queryset()
# FilterSetのインスタンスを作成し、フィルタリングを適用
# request.GET と 初期QuerySet を渡す
self.filterset = ProductFilter(self.request.GET, queryset=queryset)
# フィルタリング後のQuerySetを返す
return self.filterset.qs
def get_context_data(self, **kwargs):
# デフォルトのコンテキストデータを取得
context = super().get_context_data(**kwargs)
# フィルタリングフォームをコンテキストに追加
# テンプレートで filter.form としてアクセスできるようになる
context['filter'] = self.filterset
return context
“`
このProductListView
では、get_queryset
メソッドをオーバーライドしてフィルタリングロジックを組み込んでいます。
super().get_queryset()
で、ListView
のデフォルトのQuerySet(model = Product
で指定したProduct.objects.all()
)を取得します。- 取得したQuerySetと
self.request.GET
を使ってProductFilter
のインスタンスを作成します。この時点でフィルタリングが実行され、結果はself.filterset.qs
に格納されます。 get_queryset
はフィルタリング後のself.filterset.qs
を返します。ListView
はこのQuerySetを使用してオブジェクトリストを取得し、ページネーションなどの処理を行います。get_context_data
メソッドをオーバーライドして、テンプレートでフィルタリングフォームを表示するためにself.filterset
インスタンス全体をコンテキストに追加します。
このビューを使うようにURLconfを変更します。
“`python
products/urls.py
from django.urls import path
from . import views
urlpatterns = [
# path(‘products/’, views.product_list, name=’product_list’), # 関数ベースビューをコメントアウト
path(‘products/’, views.ProductListView.as_view(), name=’product_list’), # クラスベースビューを使用
]
“`
テンプレートは関数ベースビューの場合とほぼ同じものが使えます。filter
オブジェクトとproduct_list
オブジェクト(context_object_name
で指定した名前)がテンプレートに渡されます。
django-filter
提供のFilterView
django-filter
は、この「フィルタリングされたリストビュー」のパターンをさらに簡単に実装するためのFilterView
クラスを提供しています。これはListView
を継承し、フィルタリングのためのロジックが組み込まれています。
“`python
products/views.py
from django_filters.views import FilterView # FilterViewをインポート
from .models import Product
from .filters import ProductFilter
class ProductFilterView(FilterView):
model = Product # 対象モデル
filterset_class = ProductFilter # 使用するFilterSetクラス
template_name = ‘products/product_list.html’ # 使用するテンプレート
context_object_name = ‘product_list’ # テンプレートに渡すQuerySetの名前
# ページネーションなどもListViewと同様に設定可能
# paginate_by = 10
# FilterViewはデフォルトで context['filter'] に FilterSet インスタンスを追加してくれるため、
# get_context_data をオーバーライドする必要はありません。
# get_queryset も FilterView が内部で処理してくれるため、オーバーライド不要です。
“`
FilterView
を使う場合、model
とfilterset_class
を指定するだけで済みます。FilterView
が自動的にget_queryset
内でfilterset_class
を使ってフィルタリングを適用し、get_context_data
内で生成したFilterSet
インスタンスを'filter'
という名前でテンプレートコンテキストに追加してくれます。
URLconfも同様に修正します。
“`python
products/urls.py
from django.urls import path
from . import views
urlpatterns = [
# path(‘products/’, views.ProductListView.as_view(), name=’product_list’), # ListViewをコメントアウト
path(‘products/’, views.ProductFilterView.as_view(), name=’product_list’), # FilterViewを使用
]
“`
FilterView
は、フィルタリング機能付きリストビューを最も簡単に実装できる方法です。特別な理由がない限り、ListView
をオーバーライドするよりもFilterView
を使うことをお勧めします。
検索条件の保持とページネーション
ユーザーが検索条件を指定して結果をフィルタリングした後、ページネーションを行うと、通常は検索条件が失われてしまいます。ページネーションリンクには検索条件のGETパラメータを含める必要があります。
django-filter
は、FilterView
を使う場合に、ページネーションリンクに現在のフィルタリング条件を自動的に引き継ぐようにヘルパーを提供しています。
FilterView
を使用している場合、テンプレートでページネーションリンクを生成する際に、{{ request.GET.urlencode }}
を使うことで、現在のGETパラメータをURLに含めることができます。
“`html
{# products/templates/products/product_list.html (ページネーション部分の例) #}
{% comment %} paginate_by = 10 などと設定している場合 {% endcomment %}
{% if is_paginated %}
{% if page_obj.has_previous %}
{# 現在のGETパラメータに ‘page’ パラメータを追加/変更して次ページへのリンクを生成 #}
前へ
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
{% if page_obj.has_next %}
次へ
{% endif %}
{% endif %}
{% comment %}
上記の {% param_replace ... %}
は django-filter が提供するカスタムテンプレートタグです。
settings.py に以下の設定を追加する必要があります。
TEMPLATES = [
{
…
‘OPTIONS’: {
‘context_processors’: [
…
],
‘libraries’: {
‘query_params’: ‘django_filter.templatetags.query_params’,
}
},
},
]
テンプレートの先頭で {% load query_params %} を読み込む必要があります。
{% endcomment %}
“`
django-filter
は、ページネーションを処理するテンプレートタグquery_params
を提供しています。これを使うには、settings.py
のTEMPLATES
設定に'django_filter.templatetags.query_params'
をlibraries
として追加し、テンプレートで{% load query_params %}
を記述します。
そして、リンクのURL生成時に{% param_replace キー=値 %}
という形式でタグを使用します。これは現在のGETパラメータをすべて引き継ぎつつ、指定したキー(例: page
)の値を置き換えた新しいクエリ文字列を生成します。これにより、検索条件を保持したままページ移動できるようになります。
応用的な使い方
django-filter
は、基本的なフィルタリングだけでなく、さまざまな応用的な使い方が可能です。
関連モデルの複雑なフィルタリング
先ほど基本的な関連モデルのフィルタリングを見ましたが、さらに複雑なケースも対応できます。例えば、ManyToManyフィールドを介したフィルタリングです。
Product
モデルにTag
モデルとのManyToManyリレーションシップがあるとします。
“`python
products/models.py
… Category, Product モデルの定義 …
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
Product モデルに ManyToMany フィールドを追加
class Product(models.Model):
# … 既存フィールド …
tags = models.ManyToManyField(Tag, related_name=’products’)
# ... __str__ メソッド ...
“`
タグで商品をフィルタリングする場合、複数のタグを選択して「いずれかのタグを持つ商品」または「全ての選択されたタグを持つ商品」を検索したいといった要件が考えられます。
“`python
products/filters.py
import django_filters
from django.db.models import Q
from .models import Product, Tag
… 他のインポート …
class ProductFilter(django_filters.FilterSet):
# … 他のフィルター定義 …
# 複数のタグでフィルタリング (ModelMultipleChoiceFilter)
# デフォルトの lookup_expr='exact' は、選択された「いずれかの」タグを持つ商品を検索する (つまり __in)
tags = django_filters.ModelMultipleChoiceFilter(
queryset=Tag.objects.all(),
label='タグ',
widget=django.forms.CheckboxSelectMultiple # チェックボックスリストで表示
)
# 「全ての選択されたタグを持つ」商品を検索するカスタムフィルター (MethodFilter)
all_tags = django_filters.MethodFilter(
action='filter_by_all_tags',
queryset=Tag.objects.all(), # この queryset はフォームフィールドの選択肢に利用される
label='すべてのタグを含む',
widget=django.forms.CheckboxSelectMultiple
)
class Meta:
model = Product
fields = ['name', 'price', 'is_available', 'category', 'tags', 'all_tags']
# all_tags フィルターのカスタムメソッド
# value は選択された Tag オブジェクトの QuerySet
def filter_by_all_tags(self, queryset, name, value):
if not value:
return queryset # タグが選択されていなければQuerySetを変更せず返す
# 選択された各タグについて、そのタグを含むという条件 (tags__in=[tag]) をAND結合する
# QuerySetの filter() をチェーンすることでAND条件になる
for tag in value:
queryset = queryset.filter(tags=tag) # または tags__in=[tag]
return queryset
“`
tags
フィールドにはModelMultipleChoiceFilter
を使用しました。デフォルトでは、選択された複数のタグのいずれかを持つ商品(tags__in=[tag1, tag2, ...]
)を検索するフォームフィールドを生成します。ウィジェットをCheckboxSelectMultiple
にすると、複数のタグをチェックボックスで選択できるようになります。
all_tags
フィールドではMethodFilter
を使用し、「全ての選択されたタグを持つ」商品を検索するロジックを実装しました。カスタムメソッドfilter_by_all_tags
内で、選択された各タグについてqueryset.filter(tags=tag)
を繰り返し呼び出すことで、AND条件でのフィルタリングを実現しています。
このように、__
を使ったリレーションシップの辿り方と、適切なフィルタータイプ(特にModelChoiceFilter
, ModelMultipleChoiceFilter
, MethodFilter
)を組み合わせることで、複雑な関連モデルのフィルタリング要件にも対応できます。
複数のFilterSet
を組み合わせる
アプリケーションの規模が大きくなると、1つのモデルに対して複数の検索インターフェースが必要になる場合があります。例えば、管理者向けの全項目検索と、ユーザー向けの簡易検索などです。この場合、複数のFilterSet
クラスを定義し、ビューやテンプレートで使い分けることができます。
“`python
products/filters.py
import django_filters
… 他のインポート …
class ProductSimpleFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr=’icontains’, label=’商品名’)
# 簡易検索なので、他のフィールドは含めない
class Meta:
model = Product
fields = ['name']
class ProductAdminFilter(django_filters.FilterSet):
# 管理者向けなので、より詳細なフィルターを含める
name = django_filters.CharFilter(lookup_expr=’icontains’, label=’商品名’)
price = django_filters.RangeFilter(label=’価格帯’)
is_available = django_filters.BooleanFilter(label=’公開中’)
category = django_filters.ModelChoiceFilter(queryset=Category.objects.all(), label=’カテゴリ’)
stock = django_filters.NumberFilter(lookup_expr=’gte’, label=’在庫 (以上)’)
class Meta:
model = Product
fields = ['name', 'price', 'is_available', 'category', 'stock']
“`
ビューでは、アクセスするユーザーの権限やURLに応じて、どちらかのFilterSet
を選択して使用します。FilterView
を使う場合は、例えばget_filterset_class
メソッドをオーバーライドして動的にfilterset_class
を切り替えることができます。
“`python
products/views.py
from django_filters.views import FilterView
… 他のインポート …
from .models import Product
from .filters import ProductSimpleFilter, ProductAdminFilter # 複数のFilterSetをインポート
class ProductListView(FilterView):
model = Product
template_name = ‘products/product_list.html’
context_object_name = ‘product_list’
# filterset_class は get_filterset_class で動的に設定するので、ここでは指定しない
def get_filterset_class(self):
# 例: ユーザーがスタッフなら管理者用FilterSet、そうでなければ簡易FilterSetを使用
if self.request.user.is_staff:
return ProductAdminFilter
else:
return ProductSimpleFilter
# template_name もユーザーの権限に応じて変えることも可能
# def get_template_names(self):
# if self.request.user.is_staff:
# return ['products/product_list_admin.html']
# else:
# return ['products/product_list_user.html']
“`
これにより、異なるユーザーグループや用途に合わせて、適切で使いやすいフィルタリングインターフェースを提供できます。
検索条件の保持(セッションなど)
ユーザーがページを移動したり、一度検索フォームを閉じたりしても、直前の検索条件を保持しておきたい場合があります。これは、検索条件をセッションに保存することで実現できます。
“`python
products/views.py (FilterViewを使用している場合)
from django_filters.views import FilterView
… 他のインポート …
from .models import Product
from .filters import ProductFilter
class ProductFilterView(FilterView):
model = Product
filterset_class = ProductFilter
template_name = ‘products/product_list.html’
context_object_name = ‘product_list’
def get_queryset(self):
# リクエストパラメータを取得
# GETパラメータがあればそれを使用、なければセッションから取得
queryset = super().get_queryset()
# GETパラメータがあればセッションに保存
# 'clear'パラメータがあればセッションをクリア
query_params = self.request.GET
session_key = f'{self.__class__.__name__}_filter' # セッションキーはビューごとにユニークに
if 'clear' in query_params:
# 'clear'パラメータがあればセッションの検索条件を削除
if session_key in self.request.session:
del self.request.session[session_key]
# クリア後はGETパラメータなしでリダイレクトする(URLから?clearを消すため)
return queryset.none() # リダイレクト処理は後続で行うため、ここでは空のQuerySetを返す
if query_params:
# GETパラメータがあればセッションに保存
# QueryDictは直接シリアライズできないので、普通の辞書に変換
self.request.session[session_key] = dict(query_params.items())
elif session_key in self.request.session:
# GETパラメータがなく、セッションに保存された検索条件があればそれを使用
query_params = self.request.session[session_key]
# FilterSetのインスタンスを作成し、フィルタリングを適用
self.filterset = self.filterset_class(query_params, queryset=queryset)
return self.filterset.qs
def get(self, request, *args, **kwargs):
# get_queryset で 'clear' パラメータを処理した場合のリダイレクト
if 'clear' in request.GET:
# 現在のURLにリダイレクト(パラメータなし)
return redirect(request.path)
return super().get(request, *args, **kwargs)
# FilterViewはデフォルトでfilterインスタンスをcontextに追加してくれる
“`
この例では、get_queryset
メソッド内で以下の処理を行っています。
- まず、通常の
get_queryset
を呼び出して初期のQuerySetを取得します(ここではまだフィルタリングしません)。 - リクエストのGETパラメータを確認します。
- もしGETパラメータがあれば、それが新しい検索条件として扱われます。その条件をセッションに保存します。
- もしGETパラメータがなく、セッションに保存された検索条件があれば、その条件を読み込んで使用します。
clear
というGETパラメータがあれば、セッションの検索条件を削除し、パラメータなしのURLにリダイレクトします。- 最終的に決定した検索条件(GETパラメータまたはセッションから読み込んだもの)を使って
FilterSet
を初期化し、フィルタリングを実行します。
テンプレートで検索フォームを表示する際には、フォームの初期値としてセッションから読み込んだ値をセットする必要があります。FilterSet
は初期化時に渡されたデータをフォームにセットしてくれるので、特別な対応は不要です。検索条件をクリアするためのリンクには、例えば<a href="?clear">検索条件をクリア</a>
のように?clear
パラメータを付けます。
この方法で、ユーザーがページを移動したり、ブラウザを閉じたりしても、次回アクセス時に前回の検索条件がフォームに反映され、フィルタリングされたリストが表示されるようになります。ただし、セッションを使う場合は、ユーザーごとにセッションデータが増えるため、適切に管理する必要があります。
パフォーマンスに関する考慮事項
django-filter
は内部でDjangoのQuerySetのfilter()
メソッドを呼び出しているだけなので、フィルタリング自体のパフォーマンスはDjango ORMに依存します。しかし、django-filter
の使い方によっては、意図しないパフォーマンスボトルネックを生む可能性もあります。
データベースクエリの最適化
フィルタリング対象のモデルがForeignKeyやManyToManyFieldなどのリレーションシップを持っている場合、関連オブジェクトのフィールドをフィルタリング条件や表示項目として使う際に、N+1問題が発生する可能性があります。これを避けるためには、Djangoのselect_related()
やprefetch_related()
を適切に利用することが重要です。
これらの最適化は、FilterSet
に渡す初期のQuerySetに対して行います。
“`python
products/views.py (ListView または FilterView を使用している場合)
from django.db.models import Prefetch
… 他のインポート …
class ProductListView(FilterView):
model = Product
filterset_class = ProductFilter
template_name = ‘products/product_list.html’
context_object_name = ‘product_list’
# paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset() # FilterViewはここでフィルタリングを適用済み
# テンプレートで category や tags を表示する場合、N+1問題を防ぐために関連オブジェクトをプリフェッチ
queryset = queryset.select_related('category').prefetch_related('tags')
# 例: tags__tag_name でフィルタリングする場合は、prefetch_related('tags') が役立つ
# ただし、FilterSet の filter() メソッド自体が JOIN を行うため、
# select_related/prefetch_related は主に**フィルタリング後のオブジェクトリストをテンプレートで表示する際**に効果を発揮します。
return queryset
“`
FilterView
を使用している場合、get_queryset
メソッドはフィルタリングが適用された後のQuerySetを返します。このメソッドをオーバーライドして、プリフェッチングやセレクト関連の最適化を追加します。
例えば、テンプレートで商品のリストを表示する際にproduct.category.name
やproduct.tags.all()
のような形で関連オブジェクトにアクセスしている場合、上記のselect_related('category')
やprefetch_related('tags')
がN+1問題を解消し、データベースクエリ数を大幅に削減できます。
インデックスの重要性
フィルタリングによく使われるモデルフィールドには、データベースインデックスを作成することを強くお勧めします。インデックスがない場合、データベースはフィルタリングを行うためにテーブル全体をスキャンする必要があり、データ量が増えるにつれて検索パフォーマンスが著しく低下します。
特に、CharFilter
でのicontains
のような部分一致検索や、NumberFilter
, DateFilter
での範囲検索(gte
, lte
)は、インデックスがないと非常に低速になる可能性があります。
インデックスはモデル定義のMeta
クラスで指定できます。
“`python
products/models.py (Product モデルの Meta クラス内)
class Product(models.Model):
# … フィールド定義 …
class Meta:
# ... 既存の Meta オプション ...
indexes = [
models.Index(fields=['name']), # 名前フィールドに単一カラムインデックス
models.Index(fields=['price']), # 価格フィールドに単一カラムインデックス
models.Index(fields=['created_at']), # 作成日フィールドに単一カラムインデックス
# 複数のフィールドで複合的にフィルタリングする場合、複合インデックスも検討
# models.Index(fields=['category', 'is_available']),
]
“`
インデックスを追加または変更した後は、必ずマイグレーションを実行してください。
bash
python manage.py makemigrations
python manage.py migrate
どのようなインデックスが必要かは、アプリケーションでよく使われるフィルタリング条件によって異なります。開発中にDjango Debug Toolbarなどで発行されるSQLクエリを確認し、ボトルネックとなっているクエリがあれば、インデックスの追加を検討しましょう。
複雑なフィルタリング条件によるパフォーマンス低下
複数のフィルター条件を組み合わせたり、MethodFilter
で複雑なロジックを実装したりする場合、生成されるSQLクエリが複雑になり、パフォーマンスが低下する可能性があります。
例えば、Q
オブジェクトを多数組み合わせてAND/OR条件を構築する場合や、データベース関数 (annotate
, aggregate
) を多用するカスタムフィルターは、注意が必要です。
このような場合は、以下の点を検討してください。
- カスタムSQL: ORMで複雑なクエリを表現するのが困難または非効率な場合は、カスタムSQLクエリの使用も選択肢に入ります。(ただし、ORMの恩恵を受けられなくなるトレードオフがあります。)
- 全文検索エンジン: テキストフィールドの部分一致検索や、複数のテキストフィールドを跨いだ検索が主な要件である場合は、ElasticsearchやSolrのような全文検索エンジンを導入することを検討しましょう。
django-haystack
のようなライブラリを使うと、Djangoと全文検索エンジンを連携させやすくなります。django-filter
はOR(オブジェクトリレーショナル)フィルタリングに特化しているため、全文検索は得意ではありません。 - キャッシング: 頻繁に実行される同じ検索クエリの結果をキャッシュすることで、データベース負荷を軽減できます。Djangoのキャッシングフレームワークや、Redisなどの外部キャッシュストアを利用します。
- 背景処理: 非常に時間がかかる可能性のあるフィルタリング処理は、Celeryなどのタスクキューを使って非同期で行い、ユーザーには処理中であることを表示するなどの工夫が必要になる場合もあります。
これらのパフォーマンス対策はdjango-filter
自体というよりは、Django ORMやデータベース全般、あるいはシステムアーキテクチャの設計に関わるものですが、複雑な検索機能を実装する上では不可避の検討事項となります。
テスト方法
django-filter
を使ったフィルタリング機能をテストすることは、アプリケーションの信頼性を保証するために重要です。テストは主に以下のレベルで行えます。
FilterSet
単体テスト:FilterSet
クラスが与えられた入力データに対して正しくQuerySet
を生成するかを確認します。- ビューテスト:
FilterView
などのビュークラスが、特定のGETリクエストに対して正しいコンテキストデータ(特にフィルタリングされたオブジェクトリスト)とテンプレートを返すかを確認します。 - エンドツーエンドテスト: DjangoのテストクライアントやSeleniumなどのツールを使って、フォームへの入力、送信、表示されるリストの確認といった一連のユーザー操作をシミュレートします。
FilterSet
単体テスト
FilterSet
単体テストでは、特定のフィルタリング条件に対応するGETパラメータの辞書を作成し、それを使ってFilterSet
のインスタンスを作成します。そして、生成されたfilterset.qs
が期待するQuerySetと一致するかを確認します。
テスト用のデータ(モデルインスタンス)を事前に作成しておく必要があります。DjangoのTestCase
やTransactionTestCase
を使用し、setUp
メソッドなどでテストデータをセットアップします。
“`python
products/tests/test_filters.py
from django.test import TestCase
from products.models import Product, Category, Tag # テスト対象モデル
from products.filters import ProductFilter # テスト対象FilterSet
from django.db.models import QuerySet # QuerySetとの比較に使うかもしれない
class ProductFilterTestCase(TestCase):
@classmethod
def setUpTestData(cls):
# テストデータをセットアップ
cls.category_a = Category.objects.create(name=’Category A’)
cls.category_b = Category.objects.create(name=’Category B’)
cls.tag_1 = Tag.objects.create(name='Tag 1')
cls.tag_2 = Tag.objects.create(name='Tag 2')
cls.tag_3 = Tag.objects.create(name='Tag 3')
cls.product1 = Product.objects.create(name='Apple', price=100, stock=10, is_available=True, category=cls.category_a)
cls.product1.tags.set([cls.tag_1, cls.tag_2])
cls.product2 = Product.objects.create(name='Banana', price=200, stock=0, is_available=False, category=cls.category_a)
cls.product2.tags.set([cls.tag_2, cls.tag_3])
cls.product3 = Product.objects.create(name='Cherry', price=150, stock=5, is_available=True, category=cls.category_b)
cls.product3.tags.set([cls.tag_1, cls.tag_3])
cls.product4 = Product.objects.create(name='Date', price=300, stock=2, is_available=True, category=None)
cls.product4.tags.set([])
def test_filter_by_name_icontains(self):
# 名前で部分一致検索のテスト
data = {'name': 'a'} # 検索条件: 名前に'a'を含む
queryset = Product.objects.all()
filter = ProductFilter(data, queryset=queryset)
filtered_products = filter.qs # フィルタリング後のQuerySet
# 期待する結果: Apple, Banana
self.assertIn(self.product1, filtered_products)
self.assertIn(self.product2, filtered_products)
self.assertNotIn(self.product3, filtered_products)
self.assertNotIn(self.product4, filtered_products)
self.assertEqual(filtered_products.count(), 2)
def test_filter_by_price_range(self):
# 価格帯で範囲検索のテスト (100 <= price <= 200)
data = {'price__gte': '100', 'price__lte': '200'}
queryset = Product.objects.all()
filter = ProductFilter(data, queryset=queryset)
filtered_products = filter.qs
# 期待する結果: Apple, Banana, Cherry
self.assertIn(self.product1, filtered_products)
self.assertIn(self.product2, filtered_products)
self.assertIn(self.product3, filtered_products)
self.assertNotIn(self.product4, filtered_products)
self.assertEqual(filtered_products.count(), 3)
def test_filter_by_category(self):
# カテゴリでフィルタリングのテスト (Category A)
data = {'category': str(self.category_a.id)} # ModelChoiceFilter は ID を受け取る
queryset = Product.objects.all()
filter = ProductFilter(data, queryset=queryset)
filtered_products = filter.qs
# 期待する結果: Apple, Banana
self.assertIn(self.product1, filtered_products)
self.assertIn(self.product2, filtered_products)
self.assertNotIn(self.product3, filtered_products)
self.assertNotIn(self.product4, filtered_products)
self.assertEqual(filtered_products.count(), 2)
def test_filter_by_tags_in(self):
# 複数のタグでフィルタリング (tags__in = [tag1, tag3])
data = {'tags': [str(self.tag_1.id), str(self.tag_3.id)]} # ModelMultipleChoiceFilter は ID のリストを受け取る
queryset = Product.objects.all()
filter = ProductFilter(data, queryset=queryset)
filtered_products = filter.qs
# 期待する結果: product1 (tag1), product2 (tag2, tag3), product3 (tag1, tag3) - いずれかのタグを持つ
# product1 (tag1), product3 (tag1, tag3), product2 (tag2, tag3)
# product1: tag1, tag2 -> OK
# product2: tag2, tag3 -> OK
# product3: tag1, tag3 -> OK
# product4: [] -> NG
self.assertIn(self.product1, filtered_products)
self.assertIn(self.product2, filtered_products)
self.assertIn(self.product3, filtered_products)
self.assertNotIn(self.product4, filtered_products)
self.assertEqual(filtered_products.count(), 3)
# MethodFilter のテスト例 (filter_by_all_tags)
def test_filter_by_all_tags(self):
# 選択された全てのタグを持つ商品を検索 (tag1 と tag3 を両方持つ)
# この MethodFilter は ModelMultipleChoiceFilter をフォームに使用
# フォームから渡される値は選択された Tag オブジェクトの QuerySet (MethodFilterのactionメソッドのvalue引数)
# テストデータとしては Tag オブジェクトのリストや QuerySet を作成して渡す
selected_tags_queryset = Tag.objects.filter(id__in=[self.tag_1.id, self.tag_3.id])
data = {'all_tags': selected_tags_queryset} # MethodFilter には QuerySet を渡す(内部で action メソッドが呼ばれるため)
# ただし、通常HTTPリクエストからはIDのリストが来るので、以下のようにテストデータを作る方が現実的
data_from_request = {'all_tags': [str(self.tag_1.id), str(self.tag_3.id)]} # GETパラメータを模倣
queryset = Product.objects.all()
# Note: MethodFilter で action にメソッドを指定した場合、
# filterset は data を受け取った後、action メソッドに QuerySet と名前、生の value を渡す。
# しかし、ModelMultipleChoiceFilter と組み合わせた MethodFilter の場合、
# フォームの clean メソッドが raw value を Tag オブジェクトの QuerySet に変換してくれる。
# なので、 FilterSet には通常のリクエストと同様に生の ID リストを渡せば良い。
filter = ProductFilter(data_from_request, queryset=queryset)
filtered_products = filter.qs
# 期待する結果: product3 (tag1, tag3 を両方持つ)
self.assertNotIn(self.product1, filtered_products)
self.assertNotIn(self.product2, filtered_products)
self.assertIn(self.product3, filtered_products)
self.assertNotIn(self.product4, filtered_products)
self.assertEqual(filtered_products.count(), 1)
“`
このテストケースでは、setUpTestData
でテスト用データを作成し、各テストメソッドで異なる検索条件(data
辞書)を作成しています。そのdata
と初期QuerySetをProductFilter
に渡し、得られたfilter.qs
が期待するオブジェクトを含んでいるか(assertIn
, assertNotIn
)、件数が正しいか(assertEqual(count(), ...)
)を確認しています。
MethodFilterのようにカスタムロジックを持つフィルターをテストする場合は、そのカスタムメソッドが受け取る引数の形式(生の入力値か、クリーン済みの値かなど)を理解してテストデータを作成する必要があります。ModelMultipleChoiceFilter
と組み合わせたMethodFilter
の場合、フォームのクリーン処理を経て、カスタムメソッドには関連モデルオブジェクトのQuerySetが渡されることに注意が必要です。テストでは、HTTPリクエストで渡されるような生の形式(IDのリストなど)でFilterSet
にデータを渡し、django-filter
がそれを正しく処理してカスタムメソッドに渡せるかを確認するのがより現実的です。
ビューテスト
ビューテストでは、Djangoのテストクライアントを使って、フィルタリング条件を含むGETリクエストをシミュレートし、レスポンスを検証します。特に、テンプレートコンテキストに含まれるフィルタリングされたオブジェクトリスト(product_list
やfilter.qs
)が正しいかを確認します。
“`python
products/tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from products.models import Product, Category, Tag # モデルをインポート
TestFilterView を使用する場合
from products.views import ProductFilterView
TestListView を使用する場合
from products.views import ProductListView
class ProductFilterViewTestCase(TestCase):
@classmethod
def setUpTestData(cls):
# テストデータをセットアップ (FilterSetテストと同様)
cls.category_a = Category.objects.create(name=’Category A’)
cls.category_b = Category.objects.create(name=’Category B’)
cls.tag_1 = Tag.objects.create(name='Tag 1')
cls.tag_2 = Tag.objects.create(name='Tag 2')
cls.tag_3 = Tag.objects.create(name='Tag 3')
cls.product1 = Product.objects.create(name='Apple', price=100, stock=10, is_available=True, category=cls.category_a)
cls.product1.tags.set([cls.tag_1, cls.tag_2])
cls.product2 = Product.objects.create(name='Banana', price=200, stock=0, is_available=False, category=cls.category_a)
cls.product2.tags.set([cls.tag_2, cls.tag_3])
cls.product3 = Product.objects.create(name='Cherry', price=150, stock=5, is_available=True, category=cls.category_b)
cls.product3.tags.set([cls.tag_1, cls.tag_3])
cls.product4 = Product.objects.create(name='Date', price=300, stock=2, is_available=True, category=None)
cls.product4.tags.set([])
cls.client = Client()
cls.url = reverse('product_list') # URL名を指定
def test_no_filter(self):
# フィルター条件なしでアクセスした場合のテスト
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200) # 正常なレスポンスか
self.assertTemplateUsed(response, 'products/product_list.html') # テンプレートは正しいか
# コンテキストに含まれるオブジェクトリストが全件か
self.assertIn('product_list', response.context)
self.assertEqual(response.context['product_list'].count(), Product.objects.count())
self.assertQuerysetEqual(
response.context['product_list'].order_by('pk'), # pkでソートして比較
Product.objects.all().order_by('pk'),
transform=lambda x: x # オブジェクト自体を比較
)
# コンテキストに filter オブジェクトがあるか
self.assertIn('filter', response.context)
# filter オブジェクトの qs が全件の QuerySet と一致するか
self.assertQuerysetEqual(
response.context['filter'].qs.order_by('pk'),
Product.objects.all().order_by('pk'),
transform=lambda x: x
)
def test_filter_by_name(self):
# 名前でフィルタリングした場合のテスト
# GETパラメータを渡す
response = self.client.get(self.url, {'name': 'Apple'})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'products/product_list.html')
# フィルタリングされたオブジェクトリストが期待通りか
self.assertIn('product_list', response.context)
self.assertEqual(response.context['product_list'].count(), 1)
self.assertIn(self.product1, response.context['product_list'])
self.assertNotIn(self.product2, response.context['product_list'])
# filter オブジェクトの qs も確認
self.assertIn('filter', response.context)
self.assertEqual(response.context['filter'].qs.count(), 1)
self.assertIn(self.product1, response.context['filter'].qs)
def test_filter_by_price_range(self):
# 価格帯でフィルタリングした場合のテスト
response = self.client.get(self.url, {'price__gte': '150', 'price__lte': '250'})
self.assertEqual(response.status_code, 200)
# 期待する結果: Cherry, Banana
self.assertIn('product_list', response.context)
self.assertEqual(response.context['product_list'].count(), 2)
self.assertIn(self.product2, response.context['product_list'])
self.assertIn(self.product3, response.context['product_list'])
self.assertNotIn(self.product1, response.context['product_list'])
self.assertNotIn(self.product4, response.context['product_list'])
def test_filter_by_multiple_conditions(self):
# 複数の条件でフィルタリングした場合のテスト (名前 'a' とカテゴリ 'A')
response = self.client.get(self.url, {'name': 'a', 'category': str(self.category_a.id)})
self.assertEqual(response.status_code, 200)
# 期待する結果: Apple, Banana
self.assertIn('product_list', response.context)
self.assertEqual(response.context['product_list'].count(), 2)
self.assertIn(self.product1, response.context['product_list'])
self.assertIn(self.product2, response.context['product_list'])
self.assertNotIn(self.product3, response.context['product_list'])
self.assertNotIn(self.product4, response.context['product_list'])
def test_filter_no_results(self):
# 条件に一致する結果がない場合のテスト
response = self.client.get(self.url, {'name': 'NonExistentProduct'})
self.assertEqual(response.status_code, 200)
# 結果リストが空か
self.assertIn('product_list', response.context)
self.assertEqual(response.context['product_list'].count(), 0)
self.assertEqual(len(response.context['product_list']), 0) # len() でも確認
“`
ビューテストでは、self.client.get(self.url, data)
のように第二引数にGETパラメータの辞書を渡すことで、ユーザーがフォームを送信したときと同じリクエストをシミュレートできます。レスポンスオブジェクトからステータスコード、使用されたテンプレート、そしてテンプレートコンテキストにアクセスし、期待通りのデータが含まれているかを確認します。
assertQuerysetEqual
を使うと、2つのQuerySetが同じ要素(順序は問わない)を含んでいるかを確認できます。transform=lambda x: x
は比較対象のオブジェクト自体を比較することを意味します。順序を気にする場合は.order_by('pk')
などでソートしてから比較すると良いでしょう。
これらのテストを書くことで、django-filter
の設定が正しく行われ、ビューがユーザーの入力に対して期待通りのフィルタリング結果を返していることを確認できます。
トラブルシューティング
django-filter
を使用している際に遭遇する可能性のある一般的な問題とその解決策をいくつか紹介します。
フィルタリングが適用されない/期待と異なる結果になる
- GETパラメータのキーがFilterSetのフィールド名と一致しない:
django-filter
は、GETパラメータのキーをFilterSet
で定義されたフィルターフィールドの名前と照合します。HTMLフォームのname
属性がFilterSet
フィールド名と正確に一致しているか確認してください。 lookup_expr
の指定ミス:CharFilter
のicontains
やNumberFilter
のgte
/lte
など、意図したlookup_expr
が正しく指定されているか確認してください。Meta.fields
で辞書形式を使っている場合、生成されるフィールド名がフィールド名__lookup_expr
の形式になっているか確認してください。- データ型の不一致: GETパラメータの値は常に文字列として渡されます。
NumberFilter
やBooleanFilter
,ModelChoiceFilter
などが正しく値を解釈・変換できるか確認してください。特にカスタムMethodFilter
で生の入力値を扱う場合は、型変換を適切に行う必要があります。 - 初期QuerySetの指定ミス:
FilterSet(request.GET, queryset=...)
に渡すqueryset
が、フィルタリング対象としたい正しい初期QuerySetであるか確認してください。FilterView
を使っている場合は、get_queryset
メソッドが正しい初期QuerySetを返しているか(あるいは何もオーバーライドしていないか)確認してください。 - MultipleChoiceFilterでの値の渡し方:
ModelMultipleChoiceFilter
やChoiceFilter
で複数の値を選択可能にしている場合、GETパラメータは例えば?tags=1&tags=3
のように同じキーで複数の値が渡されます。Djangoはこれを自動的にリストとして解釈してくれますが、カスタム処理を行う場合は注意が必要です。 - MethodFilterの戻り値:
MethodFilter
で定義したメソッドは、必ずフィルタリング適用後のQuerySet
を返す必要があります。None
などを返してしまうとエラーになります。 - URLconfのミス: ビューへのURLが正しく設定されているか、特に
FilterView.as_view()
を使用しているか確認してください。 - テンプレートでのフォーム表示ミス: フォームの
name
属性が正しくHTMLに出力されているか、<form method="get">
となっているか確認してください。
フォームが表示されない/フォームフィールドが足りない
FilterSet
のMetaクラスにfields
またはexclude
が指定されていない:Meta.fields
またはMeta.exclude
を指定しない場合、django-filter
はどのフィールドのフィルターを生成すべきか判断できません。明示的にフィルターフィールドを定義している場合は、Meta.fields
は空リスト[]
または省略で問題ありませんが、そうでない場合は必須です。- 明示的に定義したフィルターがMeta.fieldsに含まれていない:
FilterSet
クラス内でフィルターフィールドを明示的に定義した場合、Meta.fields
リストには含める必要はありません。Meta.fields
はあくまでショートカットとして自動生成したいフィールドを指定するためのものです。 - テンプレートで
filter.form
を表示していない: テンプレートで{{ filter.form }}
や{{ filter.form.as_p }}
、または手動で各フィールドを表示するコードが正しく記述されているか確認してください。また、ビューのコンテキストにfilter
オブジェクトが正しく渡されているか確認してください(FilterView
を使っている場合は自動で渡されます)。 - ModelChoiceFilter/ModelMultipleChoiceFilterのquerysetが空: 関連モデルの選択肢を表示するフィルター(
ModelChoiceFilter
,ModelMultipleChoiceFilter
)で指定したqueryset
が空の場合、フォームフィールドが非表示になったり、選択肢が表示されなかったりします。データがデータベースに存在するか、queryset
の指定が正しいか確認してください。
デバッグ方法
- Django Debug Toolbar: フィルタリングが適用された後のSQLクエリを確認するのに非常に役立ちます。期待通りのJOINやWHERE句が生成されているか確認できます。N+1問題の検出にも有効です。
filter.qs
の確認: ビュー内でprint(filter.qs)
やprint(filter.qs.query)
などとして、生成されたQuerySetやSQLクエリをログやコンソールに出力して確認します。filter.form.cleaned_data
の確認: ビュー内でprint(filter.form.cleaned_data)
として、ユーザー入力がどのようにクリーンアップされ、各フィルターに渡されているかを確認します。filter.errors
の確認: フォームバリデーションエラーが発生していないか、print(filter.errors)
として確認します。request.GET
の確認: ビューの先頭でprint(request.GET)
として、ユーザーからどのようなGETパラメータが渡されているか確認します。- テンプレートタグのデバッグ: テンプレートで
{% debug %}
タグや{{ filter.form.name }}
などの個別のフィールドを表示してみて、期待通りのHTMLが出力されているか確認します。
これらのツールや手法を活用することで、問題の原因を特定しやすくなります。
他の選択肢との比較
Djangoで検索機能を実装する際に、django-filter
以外にもいくつかの選択肢があります。それぞれ得意なことや適したユースケースが異なるため、プロジェクトの要件に合わせて選択することが重要です。
手動でのフィルタリング実装 (Q
オブジェクトなど)
メリット:
* ライブラリの依存関係が増えない。
* 完全に自由にフィルタリングロジックを記述できる。
* 非常にシンプルで限定的な検索機能であれば、最も手軽に実装できる。
デメリット:
* 検索条件が増えるにつれて、ビューのコードが肥大化し、複雑になる。
* GETパラメータの取得、バリデーション、QuerySetフィルタリング、フォーム表示といった各要素を全て手動で管理する必要があり、手間がかかる。
* フォームとフィルタリングロジックの連携を手動で行う必要があり、同期ミスなどが発生しやすい。
* コードの再利用性や保守性が低下しやすい。
使い分け:
検索条件が1つか2つ程度で、フォームのカスタマイズもほとんど必要ないような非常にシンプルなケースであれば、手動実装も選択肢に入ります。しかし、検索条件が複数あり、今後増える可能性がある場合や、フォームをある程度カスタマイズしたい場合は、django-filter
を使った方が圧倒的に効率的で保守しやすくなります。
全文検索エンジン連携 (django-haystack
など)
メリット:
* 大量のテキストデータに対する高速な部分一致検索(LIKE ‘%…’よりも高速)。
* 複数のテキストフィールドを横断した関連性の高い検索結果の提示(スコアリング)。
* あいまい検索、類義語検索、ハイライト表示など、高度な検索機能を提供。
* データベースの負荷軽減。
デメリット:
* ElasticsearchやSolrなどの外部検索エンジンサーバーの構築・運用が必要になる(導入コスト、学習コストが高い)。
* データのインデックス作成(検索エンジンへの登録)処理が必要になる。
* データベースの構造的な関係性(ForeignKeyなど)に基づいた厳密な絞り込み(例: 特定カテゴリの商品のみ、価格帯での絞り込み)は、全文検索エンジンの守備範囲外または不得意な場合がある。
使い分け:
テキストフィールド(記事本文、商品名+説明など)の部分一致検索や、関連性の高い検索結果の表示が主な要件である場合に適しています。例えばブログ記事の検索、ドキュメント検索などです。django-filter
が得意とするのは、データベースの構造に基づいた条件による絞り込みです。全文検索とデータベースの絞り込みを組み合わせたい場合は、両方のライブラリ(または手動実装+検索エンジン連携)を併用することになります。django-filter
でカテゴリや価格帯で絞り込んだ後、全文検索でキーワード検索する、あるいはその逆、といったフローが考えられます。
まとめると
- シンプルで構造的な絞り込み(カテゴリ、価格帯、真偽値、関連オブジェクトなど)が中心なら →
django-filter
が最適。 - 大量のテキストデータに対する部分一致検索や関連度による並べ替えが中心なら → 全文検索エンジン連携(
django-haystack
など)を検討。 - 非常に単純な検索で、他のライブラリを使いたくない場合 → 手動実装。
多くの業務アプリケーションでは、データベースの構造に基づいた絞り込み機能が求められるため、django-filter
は非常に強力な選択肢となります。
まとめ:django-filter
で検索フォーム実装を加速しよう
この記事では、Djangoで複雑な検索フォームを簡単に実装するためのdjango-filter
ライブラリについて、その基本的な使い方から、さまざまなフィルタータイプ、フォームのカスタマイズ、ビューとの連携、応用的な使い方、パフォーマンスに関する考慮事項、テスト方法、トラブルシューティング、そして他の選択肢との比較まで、幅広く解説しました。
django-filter
を使うことで、これまで手動で書く必要があったGETパラメータの取得、バリデーション、QuerySetフィルタリング、そしてフォームの生成と表示といった一連の処理を、FilterSet
クラスの宣言的な定義に置き換えることができます。これにより、ビューのコードは大幅に簡潔になり、検索機能の実装にかかる時間と労力を削減できます。
特にFilterView
クラスは、フィルタリング機能付きのリストビューを驚くほど少ないコードで実現できる強力なツールです。また、豊富なフィルタータイプとlookup_expr
の組み合わせ、MethodFilter
によるカスタムロジック、そしてフォームの柔軟なカスタマイズ機能により、ほとんどあらゆる検索要件に対応できる拡張性を持っています。
もちろん、大規模なデータや高度なテキスト検索が必要な場合は、全文検索エンジンとの連携なども視野に入れる必要がありますが、データベースの構造に基づいた絞り込み機能に関しては、django-filter
が間違いなく第一の選択肢となるでしょう。
この記事が、あなたがDjangoアプリケーションに使いやすく、保守しやすい複雑な検索フォームを実装するための一助となれば幸いです。ぜひ、あなたのプロジェクトでdjango-filter
を活用してみてください。
次のステップ:
- この記事で紹介したコード例を実際に動かしてみる。
- あなたのプロジェクトのモデルに対して
FilterSet
を定義し、検索機能を実装してみる。 django-filter
公式ドキュメント(非常に詳細で参考になります)を読んで、さらに多くの機能やオプションを学ぶ。
django-filter
を使いこなして、ユーザーが本当に必要とする情報を簡単に見つけられる、素晴らしいアプリケーションを開発してください!
付録
主要なFilter
クラス一覧
AllValuesFilter
BaseCSVFilter
BaseInFilter
BaseRangeFilter
BooleanFilter
CharFilter
ChoiceFilter
CSVFilter
DateFilter
DateFromToRangeFilter
DateTimeFilter
DateTimeFromToRangeFilter
DurationFilter
EmptyFilter
InFilter
LookupChoiceFilter
MethodFilter
ModelChoiceFilter
ModelMultipleChoiceFilter
MultipleChoiceFilter
NumberFilter
OrderingFilter
RangeFilter
TimeFilter
TimeRangeFilter
UUIDFilter
よく使われるlookup_expr
一覧
- 等価性:
exact
,iexact
(大文字小文字区別なし) - 包含:
contains
,icontains
(大文字小文字区別なし) - 前方一致:
startswith
,istartswith
(大文字小文字区別なし) - 後方一致:
endswith
,iendswith
(大文字小文字区別なし) - 比較:
gt
(より大きい),lt
(より小さい),gte
(以上),lte
(以下) - 範囲:
range
(内部で使われる),year
,month
,day
,week
,week_day
,quarter
,time
,date
(日時から日付/時間部分を取り出す) - リストに含まれるか:
in
- NULLか:
isnull
- 正規表現:
regex
,iregex
(大文字小文字区別なし)
これらのルックアップは、対象となるモデルフィールドのタイプによって有効なものが異なります。詳細はDjangoドキュメントのQuerySet APIリファレンスを参照してください。
(記事終)