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

# Django Rest Frameworkで複数の検索キーを使って1つのレコードの取得/更新/削除をしたい
PythonのREST APIフレームワークであるDjango REST Framework（DRF）を使うときに目的の単一レコードを操作するときの小ワザです。

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

🚀 DRFについてはこちらに概要を説明した記事があるので参照
>
[Django REST Frameworkを使って爆速でAPIを実装する](https://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8)
 [Django REST framework カスタマイズ方法 - チュートリアルの補足](https://qiita.com/okoppe8/items/c58bb3faaf26c9e2f27f)
[[Django REST Framework] View の使い方をまとめてみた](https://note.crohaco.net/2018/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'`のように書けばいい。

```python:urls.py
urlpatterns = [
    path('coutries/<int:pk>/', name='detail_country')
]
```

```python: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(カリフォルニア州)`のレコードを取得したい場合。

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

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

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

> [Customizing the generic views](https://www.django-rest-framework.org/api-guide/generic-views/#customizing-the-generic-views)


```python

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`)のレコードを取得できる。

```python: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')
```

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

```python: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
```