LoginSignup
131
133

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-09-26

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インストールを行います。

$ 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
# 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です。

# 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で単体および複数のモデルを作成できるデコレーターを作成しました。

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

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のクラスです。

# 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するだけです。

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

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

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

Serializerのフィールド値を動的に決めたい

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

解決

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

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

# 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という引数で指定することも可能です。

# 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()メソッドは完全にブラックボックスの中で呼ばれています。

# views.py
class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
# 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():
        (...)

解決方法

1つ目

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

# 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のハンドリングを行う必要が出てきます。

2つ目

そこでもう一つの解決方法として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してしまうという方法です。

# 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で判定する方法でも問題はありません。

3つ目

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

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

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

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

# 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というキーワード引数に渡しています。

# 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をオーバライドしてキーワード引数から受け取ります。

# 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()などを使用する際は使うこともあるかもしれません。

以上です。

参考

131
133
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
131
133