猫でもわかるWebプログラミング

試行錯誤しながらエンジニア(プログラマー)として働く猫のブログ。技術的な話や、働き方の話、読書録とか、試行錯誤している日常の話。

Django REST framework のフィルタ

はじめに

これはこのページの日本語訳です。

www.django-rest-framework.org

フィルタリング

この章はDjangoのドキュメントの引用から始まります。

Manager から渡される QuerySet は、最初はデータベースにあるすべてのオブジェクトが入っている。そのため、そのオブジェクトのうち必要なものだけの部分集合を取得する必要がある。 — Django ドキュメント

Django REST framework の ListView は、デフォルトでは全ての QuerySet を返します。しかし、普通は、QuerySet のうち特定のものだけ返したいはずです。

QuerySet をフィルタしたい時、最も簡単なのは、 GenericAPIView を継承して get_queryset() メソッドをオーバーライド(上書き)することです。

このメソッドを上書きすることで、各 View で、様々な条件の QuerySet を返すことができます。

ログインユーザーを使ったフィルタリング

ログインしているユーザーに関連する QuerySet だけを返したいことがあります。

このような時は、 request.user が使えます。

例:

from myapp.models import Purchase
from myapp.serializers import PurchaseSerializer
from rest_framework import generics

class PurchaseList(generics.ListAPIView):
    serializer_class = PurchaseSerializer

    def get_queryset(self):
        """
        この View は、ログインしているユーザーの
        すべての支払履歴(Purchese)を返します
        """
        user = self.request.user
        return Purchase.objects.filter(purchaser=user)

URLによるフィルタリング

他にも、URLを使って QuerySet をフィルタリングする方法もあります。

例えば、以下のようにURLが設定されているとします。

url('^purchases/(?P<username>.+)/$', PurchaseList.as_view()),

このとき、URLに含まれるusernameを使って、Purchaseをフィルタリングするには以下のように書きます。

class PurchaseList(generics.ListAPIView):
    serializer_class = PurchaseSerializer

    def get_queryset(self):
        """
        この View はURLに含まれるusernameを持つuserの
        すべてのPurchaseを返します。
        """
        username = self.kwargs['username']
        return Purchase.objects.filter(purchaser__username=username)

Query parameter を使ったフィルタリング

最後に、URLの Query parameter を使ってフィルタリングされた QuerySet を返す例です。

get_queryset() メソッドを上書きし、 http://example.com/api/purchases?username=denvercoder9 のようなURLの、usernameを使い、QuerySetをフィルタリングします。

class PurchaseList(generics.ListAPIView):
    serializer_class = PurchaseSerializer

    def get_queryset(self):
        """
        username パラメータによって与えられたユーザーの
        Purchase だけを返すようにする
        """
        queryset = Purchase.objects.all()
        username = self.request.query_params.get('username', None)
        if username is not None:
            queryset = queryset.filter(purchaser__username=username)
        return queryset

ジェネリックを使ったフィルタリング

get_queryset を上書きしなくても、REST framework にはフィルタを作れる機能があります。これをジェネリックバックエンドフィルタと呼びます。*1

これを使うと、APIの結果をフィルタできるだけでなく、ブラウザでAPIを叩けるREST frameworksの機能「Browsable API」のUI上にも表示されます。

f:id:yoshiki_utakata:20200209110453p:plain

フィルタを設定する

下記のように DEFAULT_FILTER_BACKENDS を指定すると、全ての View に対して、DjangoFilterBackend というフィルタが適用されます。(DjangoFilterBackend については後に仕様などが出てきます)

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

個別のViewに設定したい場合は、GenericAPIViewクラスを継承した View に filter_backends プロパティを設定してください。

import django_filters.rest_framework
from django.contrib.auth.models import User
from myapp.serializers import UserSerializer
from rest_framework import generics

class UserListView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    filter_backends = [django_filters.rest_framework.DjangoFilterBackend]

フィルタの注意点

フィルタが View に適用されているとき、リストを返す場合だけでなく、単体のオブジェクトを返す時にもそのフィルタが適用されるので注意してください。

例えば、下記のように、id=4675 のオブジェクトを取得したとき、category や max_price に 4675 のオブジェクトが適合しない場合は、404 NotFound を返します。

http://example.com/api/products/4675/?category=clothing&max_price=10.00

get_queryset を上書き + FilterBackend

get_queryset() を上書きする方法と、 フィルタクラスを使う方法を両方合わせる事もできます。例えば、 ProductUserpurchase という多対多の関係を持っている時、このようにできます。下記の例だと、filterset_class というものを指定しつつ、get_queryset を上書きしています。

class PurchasedProductsList(generics.ListAPIView):
    model = Product
    serializer_class = ProductSerializer
    filterset_class = ProductFilter

    def get_queryset(self):
        user = self.request.user
        return user.purchase_set.all()

REST framework の詳細な仕様

DjangoFilterBackend

django-filter ライブラリには DjangoFilterBackend クラスがあり、これが REST framework で使えます。

DjangoFilterBackend を使うには、django-filter をインストールし、 django_filters を Django の INSTELLED_APPS に追加してください。

pip install django-filter

DjangoFilterBackend を設定に追加するのを忘れないようにしてください。

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

あるいは、個別のViewやViewSetに DjangoFilterBackend を追加してください。

from django_filters.rest_framework import DjangoFilterBackend

class UserListView(generics.ListAPIView):
    ...
    filter_backends = [DjangoFilterBackend]

filterset_fields を設定すれば、そのフィールドに対してのみ、完全一致でのフィルタが可能になります。

class ProductList(generics.ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['category', 'in_stock']

これにより、自動的にフィルタが使えるようになります。

http://example.com/api/products?category=clothing&in_stock=True

もっと高度なフィルタリングをしたい場合は、特殊な FilterSet クラスを実装する必要があります。django-filter のドキュメントの FilterSet を参照してください。DRF integration の節も合わせて読むとよいでしょう。

SearchFilter

SearchFilter クラスは、キーワード検索を可能にします。

SearchFilter は、search_fields に指定されたフィールドを対象に検索します。search_fields には、 CharFieldsTextField などの、文字のフィールドのみが指定可能です。

from rest_framework import filters

class UserListView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    filter_backends = [filters.SearchFilter]
    search_fields = ['username', 'email']

このようにすると、以下のような検索ができます。

http://example.com/api/users?search=russell

アンダースコア2つで ForeignKeyManyToManyField の検索も可能です。

search_fields = ['username', 'email', 'profile__profession']

デフォルトだと、検索は大文字小文字を区別せず、部分一致の検索になります。キーワードはカンマかスペースで区切ることで複数指定することもできます。複数のキーワードを指定した場合は、全てのキーワードを含むオブジェクトのみが返ってきます。

search_fields パラメータには、以下の記法が使えます。

  • ^ 前方一致
  • = 完全一致
  • @ 全文検索(MySQLをバックエンドに使っている場合のみ使えます)
  • $ 正規表現による検索

例:

search_fields = ['=username', '=email']

デフォルトでは、検索の時のクエリパラメータの名前は search ですが、設定の SEARCH_PARAMで変更できます。

リクエスト内容によって検索対象のフィールドを変更したいなど、特殊なことをしたい場合は、 SearchFilter を継承して、 get_search_fields() を上書きしてください。下記のコードは、title_only パラメータを指定された場合はタイトルのみを検索対象にするクラスです。

from rest_framework import filters

class CustomSearchFilter(filters.SearchFilter):
    def get_search_fields(self, view, request):
        if request.query_params.get('title_only'):
            return ['title']
        return super(CustomSearchFilter, self).get_search_fields(view, request)

もっと詳しく知りたい場合は Django のドキュメントを参照してください。

OrderingFilter

OrderingFilterクラスは簡単に結果を並べ替えることできます。

デフォルトだと、クエリパラメータ名は ordering になりますが、 ORDERING_PARAM で設定できます。

例えば、usernameで並べ替えたい場合はこうなります。

http://example.com/api/users?ordering=username

逆順で並べたい場合は、フィールド名の最初に - をつけます。

http://example.com/api/users?ordering=-username

複数のフィールドで並べ替えたい場合はこのようにします。

http://example.com/api/users?ordering=account,username

並べ替えできるフィールドを限定する

並べ替えできるフィールドは限定することをおすすめします。 ordering_fields で限定できます。

class UserListView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    filter_backends = [filters.OrderingFilter]
    ordering_fields = ['username', 'email']

これをしないと、passwordのような秘匿情報を並べ替えフィードに指定することができてしまい、データを推測されてしまう可能性があります。

ただし、ordering_fields を指定しない場合は、serializer_class で指定されたシリアライザで取得可能な値でのみ並べ替え可能になります。

秘匿情報を含まないので、シリアライザに関わらずすべてのフィールドを並べ替え可能にしたい場合は、__all__ を指定します。

class BookingsListView(generics.ListAPIView):
    queryset = Booking.objects.all()
    serializer_class = BookingSerializer
    filter_backends = [filters.OrderingFilter]
    ordering_fields = '__all__'

デフォルトの並び順を指定する

View に ordering プロパティを指定すると、デフォルトの並び順がそれになります。

class UserListView(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    filter_backends = [filters.OrderingFilter]
    ordering_fields = ['username', 'email']
    ordering = ['username']

フィルタのジェネリックを使ってフィルタをカスタマイズする

フィルタのジェネリックを使うことで、フィルタをカスタマイズできたり、他の開発者が作ったフィルタを使うことができます。

BaseFilterBackend を継承して、 .filter_queryset(self, request, queryset, view) を上書きします。このメソッドは、フィルタリング後の QuerySet を返すようにしてください。

リクエストしたユーザーが作成したオブジェクトだけ返したい場合はこうします。

class IsOwnerFilterBackend(filters.BaseFilterBackend):
    """
    自分のオブジェクトのみ返す
    """
    def filter_queryset(self, request, queryset, view):
        return queryset.filter(owner=request.user)

View の get_queryset() を上書きすることで同様の実装が可能ですが、 FilterBackend を使うと、簡単に様々なViewに使いまわせたり、API全体に適用させたりできます。

Browsable API インタフェースのカスタマイズ

このジェネリックは Browsable API にも有効です。to_html() メソッドを実装することで、HTML表示にも対応できます。to_html を以下の形式に従って実装してください。

to_html(self, request, queryset, view)

このメソッドは HTML の string を返してください。

ページネーションとスキーマについて

get_schema_fields() メソッドを実装することで、必要なフィールドだけ返すことができます。

get_schema_fields(self, view)

このメソッドは、 coreapi.Field のリストを返してください。

サードパーティーのパッケージ

Django REST framework filters パッケージ

github.com

関係を横断したフィルタなど、更に複雑なフィルタを簡単に実装できます。

Django REST framework full word search filter

github.com

filters.SearchFilter よりさらに高度な検索をするためのパッケージです。

Django URL Filter

github.com

人間に読みやすいURLで、かつ安全にフィルタリングをするためのパッケージです。ネストできるなど、DRFシリアライザと似たような動きをします。関係を含むようなデータのフィルタリングを簡単に実装できます。

drf-url-filters

github.com

*1:ジェネリック=一般的な、バックエンド=サーバーサイドでフィルタリングされる、的な意味合いだと思います。