Django REST framework 知っておくと少し役に立つ　小技まとめ

DjangoのRESTframeworkを使っている際に調べた内容で個人的にメモとして残しておきたい項目をまとめました。
調べたら当たり前の内容や自分で実装した項目まであります。本記事の内容でもっと良い方法があったり、そもそもこんな問題に直面することが間違っているなどの指摘がありましたら、コメント等でお願いします。


# ネストしたURLを表現したい
restframeworkのrouterを使って以下のようなrest apiを作成するのは容易です。

```
/api/v1/groups/ GET POST
/api/v1/groups/1/ GET PUT PATCH DELETE
/api/v1/members/ GET POST
/api/v1/members/1/ GET PUT PATCH DELETE
```

しかし以下のようにurlがネストしたapiをrestframeworkのrouterで作成するのは困難です。

```
/api/v1/groups/ GET POST
/api/v1/groups/1/ GET PUT PATCH DELETE
/api/v1/groups/1/members/ GET POST
/api/v1/groups/1/members/1/ GET PUT PATCH DELETE
```

## 解決方法
解決方法はdrf-nested-routersを使います。
drf-nested-routersはネストしたurlをrestframework上で簡単に実現できるライブラリです。

### 導入方法
pipインストールを行います。

```bash
$ pip install drf-nested-routers
```

### 実装
* 一階層目のrouterはrestframewrok標準のものではなく、drf-nested-routers専用のrouter(SimpleRouter)を使用します。
* 一階層目のViewをregister()メソッドで登録します。
* 二階層目のrouterを用意します。二階層目ではNestedSimpleRouterを使用して一階層目と二階層目のrouterを紐付けます。
* NestedSimpleRouterインスタンス(groups_router)のregister()メソッドで二階層目のViewを登録します。
* 最後にurlpatternsの中に一階層目のrouter.urls

```py3
# urls.py

from rest_framework_nested import routers
from .views import *

router = routers.SimpleRouter()

router.register(r'groups', GroupViewSet)
groups_router = routers.NestedSimpleRouter(router, r'groups', lookup='group')
groups_router.register(r'members', MemberViewSet, base_name='groups-members')

urlpatterns = [
	url(r'^api/v1/', include(router.urls)),
	url(r'^api/v1/', include(groups_router.urls)),
]
```

以下のように引数でそれぞれのprimary_keyを取得できます。引数のキーワード名はurls.pyで指定したlookup名+_pkです。

```py3
# views.py
class GroupViewSet(viewsets.ViewSet):
    def list(self, request):
        (...)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        group = self.queryset.get(pk=pk)
        (...)
        return Response(serializer.data)


class MemberViewSet(viewsets.ViewSet):
    def list(self, request, group_pk=None):
        members = self.queryset.filter(group=group_pk)
        (...)
        return Response(serializer.data)

    def retrieve(self, request, pk=None, group_pk=None):
        member = self.queryset.get(pk=pk, group=group_pk)
        (...)
        return Response(serializer.data)
```

# ModelViewSetで実装されたREST APIのPOSTで一度に複数のモデルを作成したい

実は標準のviews.ModelViewSetのcreate()メソッドでは一度に複数のモデルを作成することができません。複数のモデルを作成したかったらその分だけAPIを叩かなければなりません。

## 解決方法

### 作成したコード
そこで、views.ModelViewSetで単体および複数のモデルを作成できるデコレーターを作成しました。

以下のコードをコピーして適当なファイルに保存します。

```py3
from rest_framework.response import Response
from rest_framework import status

def multi_create(serializer_class=None):
    def __multi_create(function):
        def __wrapper(self, request, *args, **kwargs):
            many = False
            if isinstance(request.data, list):
                many = True
            serializer = serializer_class(data=request.data, many=many)
            if serializer.is_valid():
                serializer.save()
                headers = self.get_success_headers(serializer.data)
                data = serializer.data
                result = function(self, request, *args, **kwargs)
                if result is not None:
                    return result
                if many:
                    data = list(data)
                return Response(data,
                                status=status.HTTP_201_CREATED,
                                headers=headers)
            else:
                return Response(serializer.errors,
                                status=status.HTTP_400_BAD_REQUEST)
        return __wrapper
    return __multi_create
```

### 使い方
以下のようにmulti_createデコレーターを先ほど保存したファイルからimportしてViewSetのcreate()メソッドにつけます。引数は作成したいモデルに対応したSerializerのクラスです。

```python
# views.py

from .decorators import multi_create

class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
  
	@multi_create(serializer_class=MySerializer)
    def create(self, request):
        pass
```

あとは以下のようなリスト形式のJSONデータをPOSTするだけです。


```json
[
	{"name": "hoge"},
	{"name": "fuga"}
]
```

以下のようなレスポンスが返ってきます。

```json
[
	{
		"id": 1,
		"name": "hoge"
	},
	{
		"id": 2,
		"name": "fuga"
	}
]
```



# Serializerのフィールド値を動的に決めたい
Serializerのフィールド値を動的に決めたい場合があります。

## 解決
今回はserializers.SerializerMethodField()を使います。
serializers.SerializerMethodField()を使うことにより、メソッドの結果によってフィールドの値を決めることができます。

以下のようなModelクラスとname + _hogeを返すhoge()メソッドがあったとします。

```py3
# modles.py
class MyModel(models.Model):
    name = models.CharField(max_length=100)
    
    def hoge(self):
        return "{}_hoge".format(self.name)
```

Serializerでは以下のようにserializers.SerializerMethodField()を指定することのよりvalueフィールドの値を動的に決定します。適用されるメソッド名はget_ + フィールド名です。今回はget_value()メソッドの返却値がvalueの値となります。
また適用されるメソッド名をSerializerMethodField()のmethod_nameという引数で指定することも可能です。

```py3
# serializer.py
class MySerializer(serializers.ModelSerializer):
    value = serializers.SerializerMethodField()
    
    class Meta:
        model = MyModel
    
    def get_value(self, obj):
        return obj.hoge()
```

# ModelやSelializerなどで発生したエラーをレスポンスとして返したい
APIが叩かれViewSetのcreate()メソッドが呼ばれたとします。その際に以下のようなModelクラスのsave()メソッドでエラーが発生した場合はどうやってエラーレスポンスを行えばいいのでしょうか。
try exceptでエラーハンドリングをするにもMyViewSetクラスには自分で実装したメソッドはなく、MyModelのsave()メソッドは完全にブラックボックスの中で呼ばれています。

```py3
# views.py
class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
```

```py3
# models.py
class MyModel(models.Model):
    name = models.CharField(max_length=100)

    def save(self, force_insert=False, force_update=False,
             using=None, update_fields=None):
        if self.hoge():
            raise HogeError('hoge error.')
        super(MyModel, self).save(*args, **kwargs)

    def hoge():
        (...)
```

## 解決方法

### １つ目
一つの解決方法としては以下のようにcreate()メソッドをオーバライドしてエラーハンドリングを行うことです。

```py3
# views.py
class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer

    def create(self, request):
        try:
           super(MyViewSet, self).create(*args, **kwargs) 
        except HogeError:
            (....)
            return Response(content, status=status.HTTP_400_BAD_REQUEST)

    def update(self, request):
        try:
           super(MyViewSet, self).update(*args, **kwargs) 
        except HogeError:
            (....)
            return Response(content, status=status.HTTP_400_BAD_REQUEST)
```
この方法ではcreateとupdateの際にそれぞれ同じようにErrorのハンドリングを行う必要が出てきます。

### ２つ目
そこでもう一つの解決方法としてhandle_exception()メソッドをオーバライドする方法です。
handle_exceptionはrestframeworkの標準でエラーハンドリングを行ってくれるメソッドです。
たとえば、許可されていないHTTPメソッドを叩くと以下のようなレスポンスを返します。

```
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 42

{"detail": "Method 'DELETE' not allowed."}
```

この方法ではhandler_exceptionでexceptされていないエラーをオーバーライド先でexceptしてしまうという方法です。

```py3
# views.py
class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer

    def handle_exception(self, exc):
        try:
            return super(MyViewSet, self).handle_exception(exc)
        except HogeError:
            content = {'detail': '{}'.format(exc.args)}
            return Response(content, status=status.HTTP_400_BAD_REQUEST)
```
この方法を使うことによってこのMyViewSet内で発生したエラーを全てハンドリングすることができます。
ちなみにtry exceptではなく、excの型をisinstanceで判定する方法でも問題はありません。

### ３つ目
３つ目がcustom_exception_handler()を使う方法です。

settings.pyにこれから実装するcustom_exception_handlerのpathを記述します。

```py3
# settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
```

先ほどのpathで指定したファイルにcustom_exception_handler()を実装します。

```py3
# utils.py
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):

    response = exception_handler(exc, context)
    if isinstance(exc, HogeError):
        content = {'detail': '{}'.format(exc.args)}
        return Response(content, status=status.HTTP_400_BAD_REQUEST)
    return response
```

この方法の特徴としてはすべてのViewで発生したエラーがこのcustom_exception_handlerに集約されます。

これらの方法はそれぞれスコープが異なるので状況によって使い分けたいところです。

# Viewの値をSerializerに渡したい
## 解決方法
解決方法は考えてみれば当たり前の話でSerializerのコンストラクタ(__init__)に渡してあげれば良いのです。
今回の例ではuser_dataというキーワード引数に渡しています。

```py3
# views.py
class MyViewSet(views.ModelViewSet):
    def retrieve(self, request):
        user_data = request.GET['user_data']
        (...)
        serializer = MySerializer(My_list, many=True, user_data=user_data)
```

受け取る側では__init__をオーバライドしてキーワード引数から受け取ります。

```py3
# serializer.py
class MySerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel

    def __init__(self, *args, **kwargs):
        self.user_data = kwargs.pop('user_data', '')
        super(MySerializer, self).__init__(*args, **kwargs)
```

Viewの値をSerializerに渡すことはあまりないと思いますが、serializers.SerializersMethodFiels()などを使用する際は使うこともあるかもしれません。



以上です。



# 参考
* drf-nested-routers  
https://github.com/alanjds/drf-nested-routers
* Serializer fields - Django REST framework  
http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield
* Custom throttling response in django rest framework - CodeDump  
https://codedump.io/share/vqgOBceUoFXC/1/custom-throttling-response-in-django-rest-framework
* Exceptions - Django REST framework  
http://www.django-rest-framework.org/api-guide/exceptions/