【DRF】PUTメソッドをPOSTメソッドの代わりに使う


Django REST FrameworkでPUTメソッドでレコード(インスタンス)を作成したい場合

Django REST Framework(DRF)に限らず、REST APIでは通常、HTTPプロトコルのPOSTメソッドでインスタンス(=レコード)を作成してPUTメソッドで更新をするが、PUTで作成することはできないか調べてみたところ方法があったので備忘録として残しておく。

起きた問題とやりたいこと

レコードの更新(一度作成したレコードのデータをユーザーが編集した場合など)にPUTを使っていたが、DBに存在しないレコードをPUTで送ろうとしたところ、404エラーが返ってきた。通常はPOSTでレコードで作成するようにしているが、PUTでもレコードの作成ができるようにしたい。

もう一度、説明すると想定している挙動は次の通り。

[ こうなってほしい ]

  • 通常はPOSTでレコードを新規作成して、PUTPATCH(部分更新)でレコードの更新
  • PUTで更新しようとしたレコードがDBに存在しない場合、404エラーにならずに新規作成される

POSTとかPUT、PATCHの違いがよくわからないという場合はPUT か POST か PATCH か?を参照

解決法

公式チュートリアルのPUT as createに解決法が載ってた。

Prior to version 3.0 the REST framework mixins treated PUT as either an update or a create operation, depending on if the object already existed or not.

Allowing PUT as create operations is problematic, as it necessarily exposes information about the existence or non-existence of objects. It's also not obvious that transparently allowing re-creating of previously deleted instances is necessarily a better default behavior than simply returning 404 responses.

Both styles "PUT as 404" and "PUT as create" can be valid in different circumstances, but from version 3.0 onwards we now use 404 behavior as the default, due to it being simpler and more obvious.

If you need to generic PUT-as-create behavior you may want to include something like this AllowPUTAsCreateMixin class as a mixin to your views.

要は、「DRFのバージョン3.0以上ではPUTで存在しないレコードを更新しようとすると404ステータスが返ってくるようになってるけど、AllowPUTAsCreateMixinを使えばPUTでもPOSTのようにレコードを新規作成できるようになるよ」ということのよう。

上記リンク先のスニペットと同じものだが、必要なモジュールのインポート文が抜けていたので足したものを載せておく。これをインポートしてレコード更新に使っているViewクラスにMixinすればPUTでもレコード作成ができるようになる。フロントエンドで条件分岐でPOSTメソッドとPUTメソッドを使い分けている場合などに、PUTのメソッドだけで作成と更新の両方を済ませられるので、コードが簡潔になりそう

put_as_create.py
from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.request import clone_request


class AllowPUTAsCreateMixin(object):
    """
    The following mixin class may be used in order to support PUT-as-create
    behavior for incoming requests.
    """

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object_or_none()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)

        if instance is None:
            lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
            lookup_value = self.kwargs[lookup_url_kwarg]
            extra_kwargs = {self.lookup_field: lookup_value}
            serializer.save(**extra_kwargs)
            return Response(serializer.data, status=status.HTTP_201_CREATED)

        serializer.save()
        return Response(serializer.data)

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

    def get_object_or_none(self):
        try:
            return self.get_object()
        except Http404:
            if self.request.method == 'PUT':
                # For PUT-as-create operation, we need to ensure that we have
                # relevant permissions, as if this was a POST request.  This
                # will either raise a PermissionDenied exception, or simply
                # return None.
                self.check_permissions(clone_request(self.request, 'POST'))
            else:
                # PATCH requests where the object does not exist should still
                # return a 404 response.
                raise