Search by

    Django REST Frameworkのチュートリアル(Tutorial 6 ViewSets & Routers)をやってみた

    Django REST Frameworkの公式チュートリアル(6 ViewSets & Routers)をやってみた時の不明点、気付きをまとめておく。

    概要

    RESTFrameworkはViewSetsを扱うの抽象モデルを搭載している。ViewSetsを使うと開発者はAPIの状態とインタラクションをモデリングすることに集中できて、URLパターンの生成を自動にできて、それはもういい感じらしい。

    ViewSetsは

    • Viewclassとほとんど同等品。
    • readやupdateが提供されている。
    • getやputなどのメソッドハンドラを提供していない。
    • 現時点ではメソッドハンドラのセットに過ぎない。
    • 通常ルータークラスによってViewセットにインスタンス化される。
    • ルータークラスはURLコンフを定義する時の複雑性をハンドルする。

    ViewSetsを使うためのリファクタリング

    まずViewの書き換えから。

    まずUserListUserDetailを1つのUserViewSetにまとめます。

    (リスト系と詳細系が1つのクラスになろうとは。関数で実装する時には思いもよらなかった展開ですね。)

    Views.py
    from rest_framework import viewsets
    
    # 読み取り専用なので、`ReadOnlyModelViewSet`を使う。
    class UserViewSet(viewsets.ReadOnlyModelViewSet):
        """
        This viewset automatically provides `list` and `detail` actions.
        """
        queryset = User.objects.all()
        serializer_class = UserSerializer

    Snippetsはこんな感じ。

    Views.py
    from rest_framework.decorators import action
    from rest_framework.response import Response
    
    # 標準の読み書き命令を網羅するために`ModelViewSet`を使用している。
    #(Userの方は読み取り専用だったので`ReadOnlyModelViewSet`でした)
    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]
    
        # @actionでカスタムアクションを作成する
        # このデコレータは標準の`create/update/delete`が使えない時に使用できる。
        # デフォルトで`GET`に対応している。`POST`が必要な時は`method`引数に渡すことができる。
        # カスタムアクションのURLは自身のメソッド名に依存している。
        # 変更が必要であれば`url_path`引数で指定する。
        @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)

    ViewSetsをURLsに明示的にバインドする

    最終段階では、不要になるコードだが、この後でてくるRouterの内部はこんな感じになる、というのを解説するため?に紹介されている。ViewSetから複数のViewが作られている点に注目。

    snippets/urls.py
    
    from snippets.views import SnippetViewSet, UserViewSet, api_root
    from rest_framework import renderers
    
    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'
    })
    
    # 以下のコメントはRouterを使った場合を想定している。
    urlpatterns = format_suffix_patterns([
        path('', api_root),
        
        # プライマリキーの有無とhttpメソッドの種類から、どのViewにルーティングすればいいか判断できる。
        path('snippets/', snippet_list, name='snippet-list'),
        path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),
    
        # `@action`デコレータを使ったViewはこれで識別できる。  
        path('snippets/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
        
        # Snippetsと同様に、プライマリキーの有無でlistとdetailの違いが自動的に決まる。
        path('users/', user_list, name='user-list'),
        path('users/<int:pk>/', user_detail, name='user-detail')
    ])

    ルーターを使う

    繰り返しになるが、上記のコードは普通は書かない。 Routerクラスを使うと、下記のコード量でよくなる。 URLから必要なViewを自動で判断してくれる優れものです。

    snippets/urls.py
    from django.urls import path, include
    from rest_framework.routers import DefaultRouter
    from snippets import views
    
    # Create a router and register our viewsets with it.
    router = DefaultRouter()
    router.register(r'snippets', views.SnippetViewSet)
    router.register(r'users', views.UserViewSet)
    
    # The API URLs are now determined automatically by the router.
    urlpatterns = [
        path('', include(router.urls)),
    ]

    DefaultRouterクラスは、APIrootも自動的に作成するので、以前定義したapi_rootメソッドはViewから削除して良い。

    viewsとviewsetのトレードオフな関係

    viewsetsは非常に便利な抽象クラス。URL規則に一貫性を持たせることが出来るし、コード量も減らせる。URLコンフをせっせと作らなくてもよくなり、APIの内部を実装することに集中出来る。

    ただし「より明示的ではない」「抽象度が高い」というのが、いつも最良のアプローチとは限らない。 クラスベースか、関数ベースか、という問題の関係にも似ている。

    適切な実装方法の選択(私的意見)

    公式には「ではどんな場合に、どんな実装方法を取るべきか」という問題については見解は示されていない。 私的な見解ですが、2つの視点で判断してはどうでしょうか?(いやまだ変数はたくさんある気がする)

    プロダクトの寿命

    • 寿命が短い -> 関数
    • 寿命が長い -> クラス

    とにかく早くサンプルでもいいので作って、という場合は深く設計するより 関数ベースでざっと動くものを作ってもいいのかもしれない。 そこからある程度要件が固まってきたら、クラスベースに書き直すというのも手かも

    規模複雑性

    • 小規模単純 -> 関数
    • 大規模複雑 -> クラス

    規模が大きく、URLが混乱しそうな場合は一貫性が重要になると思うので クラスを使って綺麗に実装しておきたい気がする。

    まとめ

    同じものを作るにしても、作る状況によってベストな選択が変わってくるのではないでしょうか。技術本位でべき実装できればいいですが、手持ちのカードでベストエフォート出すのもお仕事だと思っている。

    Viewsviewsetの是非は学習コストと慣れの問題もありそうだなと思う。 正直この時点でもRouterとかは抽象度が高過ぎて腹落ちはしていない。 最初はごりごりコードを書く様な実装でもいいが、「抽象度高めれば、viewsetsで実装できる」というのを知っていることが重要かも。

    前の記事

    参考