django-filterの使い方を理解する


django-filterとは

検索条件を短いコードで書くことができます!!!

クエリパラメータのキーにモデルのフィールド名を入れて、バリュー値で検索が可能になります。

django-filter 公式ドキュメント

※この記事では、Django Rest Frameworkの使用と初期設定は終わっている想定です!!!!!!!

modelとseiralizerを定義

サンプルになりますが、このようなモデルとシリアライザを定義しておきます。

models.py
class Book(models.Model):
    """Bookモデル ※一部省略"""
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    sub_title = models.CharField(max_length=128)
    price = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
    # 
    author = models.ForeignKey(Author, on_delete=models.PROTECT, blank=True, null=True)
    publisher = models.ForeignKey(Publisher, on_delete=models.PROTECT)
serializer.py
class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

基本的な書き方

まず、django-filterの書き方はこのようになります。
検索条件などで絞り込まずに一覧の取得を行ってみます。

サンプル_view.py
from django_filters import rest_framework as filters

class FilterBook(filters.FilterSet):
    """ BookモデルのFilter """
    class Meta:
        model = Book
        fields = '__all__'

class ListBook(APIView):
    """ BookモデルのAPIView """
    def get(self, request):
        filterset = FilterBook(request.query_params, queryset=Book.objects.all())
        serializer = BookSerializer(instance=filterset.qs, many=True)
        return Response(serializer.data)

FilterSetを継承したクラスを作成して、そこでモデルを指定します。
DjangoのFormやSerializerに似ていますね。

そしてこの作成したListBookAPIのURL
/api/v1/book/list/(省いています。)
にブラウザからリクエストすると、このようなデータがレスポンスされます。
もちろんデータはサンプルです笑

検索条件を指定する

それでは検索条件を指定してリクエストする。
先ほどリクエストしたURLは/api/v1/book/list/でしたが、そこのクエリパラメータを指定します。
/api/v1/book/list/?title=testに変更してリクエストするとレスポンスが変わります。

今回のレスポンスは何も返却されませんでした。
このtitle=testというのは、Bookモデルのtitleがtestに一致するオブジェクトを返します。
その他にもtitlesub_titleに変更して、/api/v1/book/list/?sub_title=test
リクエストをしてもレスポンスは何にも返ってきません。

ただし、クエリパラメータをBookモデルに存在しないフィールドを指定すると
絞り込みができず、レスポンスにデータが返ってきます。

検索条件を変更する

先ほどは、titlesub_titleで絞り込んで
データを抽出しました。

ただ今回は、titleは検索条件でいいけど、sub_titleは検索条件として要らない!
なんてことがあるかと思います。
そんな時は、Filterクラスのfieldsで、該当のみに指定することができます。

views.py
class FilterBook(filters.FilterSet):
    class Meta:
        model = Book
        fields = ['title']

このようにすることで、先ほどはsub_titleで絞り込みできていましたが、titleでしか絞り込みができなくなりました。

検索方法をカスタマイズする

先ほどfieldsを__all__で指定した場合だと、デフォルトの検索方法になってしまい
数値の検索や、部分一致などの検索方法が使えませんでした。
(例えば、/api/v1/book/list/?price=testにリクエストしても、絞り込みはされません。)

そこで、検索方法をカスタマイズしていきます。

views.py
class FilterBook(filters.FilterSet):
    # UUID
    id = filters.UUIDFilter()

    # 部分一致
    title = filters.CharFilter(lookup_expr='icontains')

    # 金額を調べる
    price = filters.NumberFilter()
    price__gt = filters.NumberFilter(field_name='price', lookup_expr='gt')
    price__lt = filters.NumberFilter(field_name='price', lookup_expr='lt')

    class Meta:
        model = Book
        fields = []

カスタマイズの内容です。
filters.UUIDFilter()は、UUIDへの対応です。IDで検索かけても引っかかるようになりました。
filters.CharFilter(lookup_expr='icontains')
部分一致にしました。このlookup_exprの引数は、Djangoのfield lookups
と同じ指定ができます。
filters.NumberFilter(field_name='price', lookup_expr='gt')
は数値の対応と範囲を指定しています。
field_nameは、モデルの対象フィールドを指定します。
通常はフィルターのフィールド名とモデルのフィールド名で一致していれば、指定しなくても問題ありません。


それではカスタマイズ後にリクエストをします。
api/v1/book/list/?title=sql
api/v1/book/list/?price_gt=3200&price_lt=4000
にリクエストしても、データがレスポンスされるようになりました。

リレーションにも対応する

ForeignKeyで結びついたモデルも検索条件にしたい場合は、
field_nameに指定します。
指定方法はdjangoのlookup式で対応できます。

views.py
class FilterBook(filters.FilterSet):
    author_last_name = filters.CharFilter(field_name='author__last_name',lookup_expr='icontains')

デフォルトのフィルターをカスタマイズする

デフォルトのフィルターでは、CharFilterが完全一致だったりでフィールドごとにカスタマイズする必要がありました。
フィールドごとにカスタマイズするのではなく、CharFilterのデフォルトをカスタマイズしようということです。

views.py
class FilterBook(filters.FilterSet):

    class Meta:
        model = Book
        fields = ['title']
        filter_overrides = {
            models.CharField: {
                'filter_class': filters.CharFilter,
                'extra': lambda f: {
                    'lookup_expr': 'icontains',
                },
            },
        }

class Meta:filter_overrridesとデフォルトの設定にしたい内容を記載します。
今回これで、モデルにCharFiledを使っているフィールドは、デフォルトで全て部分一致になりました。

独自のカスタマイズをする

絞りこむ際に独自の検索を行うことができます。
ハイフンを取り除き、部分一致にする例を作成しました。

views.py
class FilterBook(filters.FilterSet):
    title = filters.CharFilter(method='title_custom_filter')

    class Meta:
        model = Book
        fields = []

    @staticmethod
    def title_custom_filter(queryset, name, value):
        # nameはフィールド名前
        # valueは検索する値
        # name__icontainsが部分一致
        lookup = '__'.join([name, 'icontains'])
        # ハイフンを消す
        replaced_value = value.replace('-', '')
        return queryset.filter(**{
            lookup: replaced_value,
        })

CharFilterの引数にメソッドの名前を文字列で渡すと、そのメソッドが検索時に実行されます。
nameにフィールド名、value検索のパラメータが渡されます。
なのでそこを変更すれば、自由に検索条件が作成できる分けです。

今回では、例えば
/api/v1/book/list/?title=gと同じように
/api/v1/book/list/?title=g---
にリクエストを送信してもレスポンスがくるようになりました。


この他の詳しい情報は、
ぜひ公式ドキュメント
ご確認いただければとおもいます!!!!!!!!!!!!!!!!!