DRFチュートリアル要旨3


APIクラスベースビューについて

基本

前回までは関数ベースでのAPIビューを見てきたが、今回からクラスベースビューを見ていく。
関数ベースでは@api_view[]を用いたが、クラスベースビューでAPIビューを書きたいときはAPIViewを継承する。

要旨2の内容をクラスベースビューに置き換え

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status


class SnippetList(APIView):
    """
    List all snippets, or create a new snippet.
    """
    def get(self, request, format=None):
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class SnippetDetail(APIView):
    """
    Retrieve, update or delete a snippet instance.
    """

    # インスタンス単体を取得
    def get_object(self, pk):
        try:
            return Snippet.objects.get(pk=pk)
        except Snippet.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        snippet = self.get_object(pk)
        serializer = SnippetSerializer(snippet)
        return Response(serializer.data)

    def put(self, request, pk, format=None):
        snippet = self.get_object(pk)
        serializer = SnippetSerializer(snippet, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        snippet = self.get_object(pk)
        snippet.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)


また、クラスベースビューへリファクタリングしたのでsnippets/urls.pyも書き換える必要がある。


from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path('snippets/', views.SnippetList.as_view()),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)


mixinsについて

クラスベースビューでのAPIの利用は通常のクラスベースビューのようにフレームワーク側が用意する汎用ビューを継承して使うことができる。
汎用ビューを使うと例えば先程のコードが以下のように圧縮されることになる。

汎用ビューを使ってリファクタリング

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


genericsモジュールから各汎用ビューを継承していく(実際に書く際にはfrom rest_framework.generics import 各汎用ビュー……と読み込んでいく)。
ListCreateAPIViewはList、Createの機能を持ったビューで、RetrieveUpdateDestroyAPIViewはUpdate、PUT、Deleteの機能を持ったビューになる。
これらはそれぞれListAPIView(一覧取得) + CreateAPIView(登録)RetrieveAPIView(取得) + UpdateAPIView(更新) + DestroyAPIView(削除)といったようにそれぞれの機能を持った汎用ビューを統合したものである。
こうやって書くとリクエストの内容に応じてフレームワーク側が自動的に役割を切り替えてくれるという寸法。
例えば、SnippetListクラスに対してGETのリクエストが来た場合はList、つまりは一覧表示の処理を行ってくるということになる。

そして、これの前段階としてmixinsでのクラスベースの書き方があるので理解を深めるためにも確認してみる。

mixinsで書いたクラスベースAPI

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics

class SnippetList(mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)


class SnippetDetail(mixins.RetrieveModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.DestroyModelMixin,
                    generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)



このコードをみると、先程の汎用ビューがどのような働きをしているのかというのがわかりやすくなると思う。
ポイントとしては実は先程の汎用ビューを含めてGenericAPIViewというビューが大本でそれを継承した派生クラスであるというところである。
当のGenericAPIView自身もAPIViewを継承したクラスである……ということはおいておいて、GenericAPIViewは自身はDjangoにおけるGenericViewのように枠組みは用意してあげるからあとは自分で作ってねとフレームワーク側から渡される白図面のようなものです。
自作する場合はGenericAPIViewmixinsの機能を1つ以上組み合わせて作っていくということになり、それをやっているのがこのコードということになります。
また当然、この場合処理にあたるメソッドも自分で定義する必要があります。
上記の例だと例えばRetrieveModelMixinを継承した場合は、.retrieve()と1件取得のメソッドが使えるようになるのでそれを使って何か取得するような処理を書かないといけない。
対して先程のListCreateAPIViewなどはフレーム側が「まあよくある組み合わせだよね」と用意してくれたいわばプリセットのようなものになる。
なので、用途がはっきり決まっているので先述のように処理を書かなくてもフレームワーク側で判断してくれるということになる。
こうみるとそれでは最初から汎用ビューだけでいいのでは? と思うところだが、mixinsという仕組みが何故あるのかというのを自分なりに考えると、CRUDという仕組みにその要因があるのだと思う。
CRUDはシンプルに機能を実装できる反面、複雑な処理のハンドリングをやるのに向いてないという弱点があると私は思っている。
例えばここでは詳しく触れないが、GenericAPIViewを継承したModelViewSetというものがある。
これを継承するとmixinsのすべての機能を持ったビューを作ることができる。
で、そのビューでユーザー登録をCRUDで作ろうとした時、ソーシャルログインを導入するからCreateの部分だけはdjango-allauthで作りたいということを思ったとする。
ところがCRUDでビュー(コントローラー)を作るときの決まりごとのようなものとして使わないメソッド(文脈の都合上、以後ここではmixinとする)は削除するというものがある。余計なものは残しておかないということだ。
その決まり事に従おうとするとこのケースでは詰んでしまうので、そういうときのためにmixinsを用いてビューをカスタマイズできるようにしているのだろう。
余談ではあるが、実際にはビューをカスタマイズする他にもHTTPリクエストを制限したりするなど他にも方法はあるようだ。

閑話休題、最後にGenericAPIViewを用いる際には必ず


    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

queryset = モデルのインスタンス及びserializer_class = モデルのシリアライザーを明示しておかないといけない。
もちろん継承先のクラスにも同じことが言えるので忘れないようにしたい。

参考

DRFのGeneric viewの使い方
DRF公式より ViewSets
DRF公式より Generic views