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

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


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

[django-filter 公式ドキュメント](https://django-filter.readthedocs.io/en/stable/index.html)

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

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

```python: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)
```
```python:serializer.py
class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'
```

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

```python:サンプル_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/`(省いています。)
にブラウザからリクエストすると、このようなデータがレスポンスされます。
もちろんデータはサンプルです笑

![スクリーンショット 2020-10-21 19.38.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563930/3c85dd1f-947d-2134-ae9c-8c0a23a6974f.png)


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

![スクリーンショット 2020-10-21 19.48.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563930/9d73955d-89ea-42c4-f50c-a2b9e6d98849.png)


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

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

#検索条件を変更する

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

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

```python:views.py
class FilterBook(filters.FilterSet):
    class Meta:
        model = Book
        fields = ['title']
```
このようにすることで、先ほどは`sub_title`で絞り込みできていましたが、`title`でしか絞り込みができなくなりました。

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

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

```python: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](https://docs.djangoproject.com/ja/3.1/ref/models/querysets/#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`
にリクエストしても、データがレスポンスされるようになりました。

![スクリーンショット 2020-10-21 20.39.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563930/d996b2b8-0575-5e44-b1c5-53a32f9434fb.png)

#リレーションにも対応する
ForeignKeyで結びついたモデルも検索条件にしたい場合は、
`field_name`に指定します。
指定方法はdjangoの[lookup式](https://docs.djangoproject.com/ja/3.1/ref/models/lookups/#module-django.db.models.lookups)で対応できます。

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

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

```python: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`を使っているフィールドは、デフォルトで全て部分一致になりました。


#独自のカスタマイズをする
絞りこむ際に独自の検索を行うことができます。
ハイフンを取り除き、<b>部分一致</b>にする例を作成しました。

```python: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---`
にリクエストを送信してもレスポンスがくるようになりました。

![スクリーンショット 2020-10-22 1.38.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563930/32996fb1-f4d4-5b19-0ce8-f1f74f39598d.png)


---
この他の詳しい情報は、
ぜひ[公式ドキュメント](https://django-filter.readthedocs.io/en/stable/index.html)を
ご確認いただければとおもいます!!!!!!!!!!!!!!!!!