Django REST framework で django-filter を使う

## この記事について
Django REST framework に検索機能を楽に追加できるプラグイン「django-filter」の使い方をまとめました。

## django-filter とは

少ないコーディングで検索機能を追加するdjangoのプラグインです。
組み込みのFilterSetより柔軟な設定が可能です。

公式ドキュメント
https://django-filter.readthedocs.io/en/master/

Django REST framework に触れていない人は、まずこちらのチュートリアル記事からどうぞ。
[Django REST Frameworkを使って爆速でAPIを実装する](https://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)

## サンプルコード

チュートリアルのサンプルコードを省略したものを使います。

パッケージのバージョン 
Django (2.0.1)
django-crispy-forms (1.7.0)
django-filter (1.1.0)
djangorestframework (3.7.7)

`pip install` でよしなに準備します。

```py:project/settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'django_filters', 
    'crispy_forms',
    'sample',
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) 
}
```

```sample/models.py
from django.db import models

# Create your models here.
# テスト結果の記録をイメージ
class Exam(models.Model):

    LEVEL_SET = (
            ('easy', "初級"),
            ('normal', "中級"),
            ('hard', "上級"),
    )
    
    fullname = models.CharField(max_length=32)
    level = models.CharField(choices=LEVEL_SET, default='easy', max_length=8)
    score = models.IntegerField()
    is_checked =  models.BooleanField()
    created_at = models.DateTimeField(auto_now_add=True)
```

```py:sample/views.py
# Create your views here.

from django_filters import rest_framework as filters 
from rest_framework import viewsets
from rest_framework import serializers
from .models import Exam


class ExamSerializer(serializers.ModelSerializer):
    class Meta:
        model = Exam
        fields = ('__all__')

class ExamViewSet(viewsets.ModelViewSet):
    queryset = Exam.objects.all()
    serializer_class = ExamSerializer

```


```project/urls.py
from django.conf.urls import url, include
from django.contrib import admin

from rest_framework import routers
from sample.views import ExamViewSet

router = routers.DefaultRouter()
router.register(r'exam', ExamViewSet)

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api/', include(router.urls)),
]

```


## Browsable API画面

フィルタ機能が有効になると、ブラウザでWebAPIに直接アクセスしたときに出てくるBrowsable API 画面 に「Filter」ボタンが追加されます。ここをクリックすると検索用ウィンドウからAPIを試すことができます。 
※crispy_formsを追加しておくと、ここのフォームがbootstrap対応になります。

## 文法

### フィルタセットの文法

ViewSet(WebAPI)にfilter_fieldsを定義して簡潔に使う方法もありますが、完全一致の検索にしか使えません。
部分一致や範囲を指定するにはFilterSetを継承したフィルタセットを作成し、ViewSetのfilter_classで読みこみます。


```py:sample/views.py
from django_filters import rest_framework as filters 
from rest_framework import viewsets
from rest_framework import serializers
from .models import Exam

# FilterSetを継承したフィルタセット(設定クラス)を作る
class ExamFilter(filters.FilterSet):

    # フィルタの定義
    fullname = filters.CharFilter(name="fullname", lookup_expr='contains')
    score_gt = filters.NumberFilter(lookup_expr='gt')

    class Meta:
        model = Exam
        # フィルタを列挙する。
        # デフォルトの検索方法でいいなら、モデルフィールド名のフィルタを直接定義できる。
        fields = ['fullname', 'score_gt'] 

class ExamViewSet(viewsets.ModelViewSet):
    queryset = Exam.objects.all()
    serializer_class = ExamSerializer
    # フィルタセットの指定
    filter_class = ExamFilter 
```

### フィルタの定義

(公式) https://django-filter.readthedocs.io/en/latest/ref/filters.html#filters

基本文法
`フィルタ名= filters.CharFilter(name="fullname", lookup_expr='contains')`

・name: モデル内の検索対象フィールド ※デフォルトはフィルタ名
・lookup_expr 検索条件を指定する ※デフォルトはフィルタクラスによって異なる
・method: 独自の検索処理を定義できる（後述）

lookup_exprはdjangoのクエリ構文と同じ表現で検索方法を指定する。

####lookup_exprに指定できる値の一覧（一部）

・テキスト

|lookup_expr|検索方法|
|:--|:--|
|exact|完全一致|
|contains|部分一致|
|startswith|前方一致|
|endswith|後方一致|
|regex|正規表現|

・数値・日時・テキスト（アスキー順）

|lookup_expr|検索方法|
|:--|:--|
|gt|超|
|lt|未満|
|gte|以上|
|lte|以下|

日時

・時刻の一部に一致する検索が可能

|lookup_expr|検索方法|
|:--|:--|
|date|日付(YYYY-M-D)|
|year|年(YYYY)|
|month|月(M)|
|day|日(D)|
|week_day|(1-7 日曜が1)|
|hour|時(H)|
|minute|分(m)|
|second|秒(s)|

##主なフィルタの種類

よくつかいそうなフィルタの定義例。これ以外にも多くの種類がある。詳しくは公式参照。
https://django-filter.readthedocs.io/en/latest/ref/filters.html#filters

###CharFilter

テキスト検索

```py
fullname = filters.CharFilter(lookup_expr='contains')
```
###NumberFilter 

数値検索

```py
score = filters.NumberFilter(lookup_expr='gt')
```


###BooleanFilter

Bool値検索

```py
is_checked = filters.BooleanFilter()
```
※パラメータはtrueかfalseを指定

###DateTimeFilter
日付検索

```py
created_at = filters.DateTimeFilter(lookup_expr='gt')
```
パラメータは「YYYY-MM-DD HH24-mm-SS」または「YYYY-MM-DD」で指定

###RangeFilter
範囲検索

```py
score_range = filters.RangeFilter(name='score')
```

フィルタ名に「_0」「_1」がついたパラメータが追加され、それぞれ開始値と終了値を扱う。
片方だけ設定してもＯＫ。

### ChoiceFilter
選択肢検索

```py
level = filters.ChoiceFilter(choices=Exam.LEVEL_SET)
```
値のエイリアスで検索する。choicesの指定にはModelのchoicesの設定を流用する。

### MultipleChoiceFilter 

```py
level = filters.MultipleChoiceFilter(choices=Exam.LEVEL_SET)
```
同名のパラメータを複数回使ったＯＲ検索が可能になる

### 並べ替え
https://django-filter.readthedocs.io/en/latest/ref/filters.html#orderingfilter

OrderingFilterを使って並べ替え（ソート順）の指定ができる。
フォート方法のフィールドはfieldsに(モデルフィールド,パラメータ名)のタプル配列で指定する

```py
order_by = filters.OrderingFilter(
        # tuple-mapping retains order
        fields=(
            ('fullname', 'fullname'),
            ('score', 'score_sort'),
        ),
    )
```

残念ながら複数のフィールドを使った並べ替えはできないので、やりたい時は下のカスタマイズ法でやる

### 独自の検索処理を行う方法

フィールドの method 要素を使って独自のフィルター処理を定義できる。
以下は並べ替えた後、指定した数だけ上位件数を取得する例

http://django-filter.readthedocs.io/en/latest/ref/filters.html?highlight=method#method

```py
class ExamFilter(filters.FilterSet):

    def get_top(self, queryset, name, value):
        return queryset.all()[:value]

    order_by = filters.OrderingFilter(
            # tuple-mapping retains order
            fields=(
                ('fullname', 'fullname'),
                ('score', 'score'),
            ),
        )

    top = filters.NumberFilter(method='get_top')

    class Meta:
        model = Exam
        fields = ['order_by','top']
```