【DRF】複数の検索キーを使って目的のレコードを取得する方法


Django Rest Frameworkで複数の検索キーを使って1つのレコードの取得/更新/削除をしたい

PythonのREST APIフレームワークであるDjango REST Framework(DRF)を使うときに目的の単一レコードを操作するときの小ワザです。

DRFについてはある程度知識があることが前提。

🚀 DRFについてはこちらに概要を説明した記事があるので参照

Django REST Frameworkを使って爆速でAPIを実装する
Django REST framework カスタマイズ方法 - チュートリアルの補足
[Django REST Framework] View の使い方をまとめてみた

単一の検索キーでレコードを操作したい場合

汎用API ViewなどのViewに関わるクラスにはlookup_fieldというクラス変数が設定されているので、これを利用する。RetrieveAPIView(取得)やUpdateAPIView(更新)、DeleteAPIView(削除)などで単一のインスタンス(=レコード)を操作するときに使用される。

デフォルトではpk(プライマリーキー)が割り当てられているので、継承して使う場合は明記する必要はない。

例えば、Countryというモデルがあった時に、id = 1(us)のレコードを取得したい場合は、下のようなViewクラスを書いて、urls.py~coutries/1/というエンドポイントにGETメソッド経由でHTTPリクエストを送れば取得できる。

ちなみに検索キーをpkから他のフィールド(例えばname)に変更したい場合はlookup_field = 'name'のように書けばいい。

urls.py
urlpatterns = [
    path('coutries/<int:pk>/', name='detail_country')
]
views.py
from rest_framework import generics


class CountryRetrieveAPIView(generics.RetrieveAPIView):
    queryset = Country.objects.all()
    serializer_class = CountrySerializer
    # デフォルトでpk(id)が設定されているので、pkを指定する場合は書かなくてもいい
    lookup_field = 'pk'

複数の検索キーでレコードを操作したい場合

しかし、次のようにエンドポイントに複数のキーが入っている場合がある。

例えば、外部参照キーcountry=1(us)のStateというテーブルからid=6(カリフォルニア州)のレコードを取得したい場合。

urls.py
urlpatterns = [
    # `country`と`pk`の2つのキーを使って単一レコードを取得したい
    path('coutries/<int:country>/states/<int:pk>/', name='detail_state')
]

こんな場合は上のviews.pyのままではうまくいかない。

汎用APIViewには目的のクエリセットを返すget_queryset()というメソッドが用意されているので、これをオーバーライドして、ORMで抽出条件を調整すればよいが、面倒に感じる場合はDRF公式ドキュメントに載っている次のMixinクラスを利用すると簡単に目的のクエリセットを取得できるのでおすすめ。

Customizing the generic views


class MultipleFieldLookupMixin(object):
    """
    Apply this mixin to any view or viewset to get multiple field filtering
    based on a `lookup_fields` attribute, instead of the default single field filtering.
    """
    def get_object(self):
        queryset = self.get_queryset()             # Get the base queryset
        queryset = self.filter_queryset(queryset)  # Apply any filter backends
        filter = {}
        for field in self.lookup_fields:
            if self.kwargs[field]: # Ignore empty fields.
                filter[field] = self.kwargs[field]
        obj = get_object_or_404(queryset, **filter)  # Lookup the object
        self.check_object_permissions(self.request, obj)
        return obj

このMixinを多重継承して利用する。

これでlookup_field(単数形)の代わりにlookup_fields(複数形)が使えるようになるので、タプルかリストで値をセットすればOK。

これで~coutries/1/states/6/にGETリクエストを送るとアメリカ合衆国(country_id=1)かつカリフォルニア州(id=6)のレコードを取得できる。

views.py
from django.shortcuts import get_object_or_404
from rest_framework import generics

# MultipleFieldLookupMixinを多重継承して使う
class StateRetrieveAPIView(MultipleFieldLookupMixin, generics.RetrieveAPIView):
    queryset = State.objects.all()
    serializer_class = StateSerializer
    # エンドポイントに含まれる複数の検索キーを指定できる
    lookup_fields = ('country', 'pk')

さらに次のようにして多重継承しただけのクラスを作って、他モジュールからインポートして使うようにカスタマイズもできる。

common_views.py
from django.shortcuts import get_object_or_404
from rest_framework import generics

class MultipleFieldLookupMixin(object):
    """
    Apply this mixin to any view or viewset to get multiple field filtering
    based on a `lookup_fields` attribute, instead of the default single field filtering.
    """
    def get_object(self):
        queryset = self.get_queryset()             # Get the base queryset
        queryset = self.filter_queryset(queryset)  # Apply any filter backends
        filter = {}
        for field in self.lookup_fields:
            if self.kwargs[field]: # Ignore empty fields.
                filter[field] = self.kwargs[field]
        obj = get_object_or_404(queryset, **filter)  # Lookup the object
        self.check_object_permissions(self.request, obj)
        return obj

# インポートして使える
class BaseRetrieveAPIView(MultipleFieldLookupMixin, generics.RetrieveAPIView):
    pass

# インポートして使える
class BaseRetrieveUpdateDestroyAPIView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView):
    pass