DRFチュートリアル要旨6


ViewSetsについて

何をやるのか

DRFでやることは要旨5までで終わっている。
しかし、DRFではルーティングまでフレームワーク側が自動で行ってくれる仕組みがあるのでそれをやるとどうなるのか確認してみる。

ViewSetsを導入する


from rest_framework import viewsets

class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    This viewset automatically provides `list` and `detail` actions.
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

UserList及びUserDetailを統合する。
どちらもリクエストとしてはGETのみでつまり読み取り専用でよいので、ReadOnlyModelViewSetを使う。
querysetとserailizerは同じこれまでと変わらない。
同じようにSnippetList及びSnippetDetailも統合する。


from rest_framework.decorators import action
from rest_framework.response import Response

class SnippetViewSet(viewsets.ModelViewSet):
    """
    This viewset automatically provides `list`, `create`, `retrieve`,
    `update` and `destroy` actions.

    Additionally we also provide an extra `highlight` action.
    """
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

    # detailはそのアクションがListなのかDetailなのかを示すオプション、TrueだとDetail。
    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)


今度はリクエストにPOSTがあり、読み書きができるようにしないといけないのでModelViewSetを使う。
@actionはカスタムアクションで、ViewSetsで提供されるcreate/update/deleteの機能から外れたアクションを行いたいときに定義する。
今回は以前までのSnippetHighlightクラスで行っていた処理をSnippetViewSetに統合するために定義する。
@actionについて補足をすると、デフォルトで対応しているリクエストはGETのみである。
他に必要なときは@actionの引数にmethods=['put']という形で引数を渡す。
また、URLパスはメソッドの名前に準ずることになるので、変更したい場合は同じように引数を渡す(ex. @action(url_path = 'get-highlight'))

Routerクラスを使う

さて、このViewSetsという仕組みはこれまでのviewsと違ってメソッドハンドラを持たない。
つまり、このままだとリクエストに応じてどのメソッドを使うかということが定まっていないことになる。
そのためにurls.pyにRouterクラスを用いるのだが、これが難解なことにそのまま書いてしまうとRouterクラスが何をやっているのかさっぱりわからないのでまずRouterクラスが何をやっているのかということを明示化してみる。
わかりやすく要旨5までのurls.pyと明示化した場合のコードとRouterクラスを用いたurls.pyとを並べてみる

ViewSetを使わない場合のルーティング


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

urlpatterns = format_suffix_patterns([
    path('', views.api_root),
    path('users/', views.UserList.as_view(), name='user-list'),
    path('users/<int:pk>/', views.UserDetail.as_view(), name='user-detail'),
    path('snippets/', views.SnippetList.as_view(), name='snippet-list'),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view(), name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view(), name='snippet-highlight'),
    path('api-auth/', include('rest_framework.urls')),
])

Routerクラスの処理を明示化したもの


from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers
from django.conf.urls import include

snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create',
})

snippet_detail = SnippetViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy',
})

snippet_highlight = SnippetViewSet.as_view({
    'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])

user_list = UserViewSet.as_view({
    'get': 'list'
})

user_detail = UserViewSet.as_view({
    'get': 'retrieve'
})

urlpatterns = format_suffix_patterns([
    path('', api_root),

    # 主キーの有無、及びリクエストの種類でViewへのルーティングを判別する。
    path('snippets/', snippet_list, name='snippet-list'),
    path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),

    # こちらは@actionでのViewの識別。highlightというパスが入っているかどうかとリクエストの種類で判別される
    path('snippets/<int:pk>/highlight/',snippet_highlight, name='snippet-highlight'),

    # snippetsに同じ
    path('users/', user_list, name='user-list'),
    path('users/<int:pk>/', user_detail, name='user-detail'),

    path('api-auth/', include('rest_framework.urls')),
])

Routerクラスを使用した場合

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views

# 使うルータークラスのインスタンスを定義し、register()でURLのルートに当たる部分(例えば~/usersと/users/<int:pk>/のようなルーティングを作るのであればr`users`と定義する)と使うViewSetを引数に指定する。
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)


urlpatterns = [
    # 基本はこれだけでいい
    path('', include(router.urls)),
    # 認証用のルーティングはCRUDの外のことなので個別に設定
    path('api-auth/', include('rest_framework.urls')),
]

明示化したコードを見てみるとわかるように、RouterクラスはまずURLの名前で(snippet_listなど)でどのViewSetを使うか判断して、さらにその先でリクエストに応じて行うアクションを決定している。
ViewではViewがリクエストを判断して、urls.pyに定義されたルーティングに沿ってページを表示しているのとは違って、今度はurls.py側がメソッドハンドラを行っていることがわかる。
また、1つのViewSetが複数のViewを作っているということもこれを見るとわかる。
これを踏まえて、Routerクラスを見てみるといかにコードが省かれているかがわかると思う。
また、Routerクラスには提供されるアクションによって異なるインスタンスがある。
例えば今回使うDefaultRouterは前回まで@api_viewデコレーターで定義していたapi_rootメソッドに相当する役割を内包しつつ、またリレーションも前回やったハイパーリンクでのものとなっている。
また、このインスタンスでは前回までのformat=jsonに相当する役割も自動で行ってくれるので特に定義しなくてもhttp://localhost:8000/snippets/1.jsonなどと叩くとjson形式で返してくれる。
他にも今挙げた機能が省かれたSimpleRouterというインスタンスもあれば、自分でRouterクラスを定義し、独自のインスタンスを定義することもできる。
詳しくは公式のドキュメントへ

ViewsとViewSetsはトレードオフの関係であるということ

ちゃんと使えれば便利なViewSetsという仕組みであるけれども、Routerクラス見ればわかるようにその処理は簡略化され過ぎていてフレームワークに対する理解が十分でない人間にとっては抽象度が高すぎて、逆に可読性が下がるというデメリットがある。
もっともフレームワークの理解が甘いというのは褒められたものではないのでViewSetsでの実装くらいでこんなことを言っていたら話にならないのかもしれないが、初学者が安易に使うのは自分がどういう処理をどう書いているのかということがわかりづらくなるので罠であると言ってもいいと思う。
反面、きちんと理解して使えば単純なCRUDであれば要旨5まであれだけ回り道をしてきたものが、たった数行で終わってしまうという便利さもある。
なので、まずは関数ベース及びクラスベースビューでの実装に慣れることが第一で、それを以てリファクタリング、あるいは次の実装でViewSets使えると判断したときにスムーズに扱えるようになるようにというのが私達初学者の目指すところなのだと感じた。

参考

DRF公式
DRF公式 ViewSets
DRF公式 Routerクラス