DRFチュートリアル要旨6

## ViewSetsについて

#### 何をやるのか

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


#### ViewSetsを導入する

```python

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`も統合する。

```python

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`とを並べてみる

<details><summary>ViewSetを使わない場合のルーティング</summary><div>

```python


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')),
])

```

</div></details>

<details><summary>Routerクラスの処理を明示化したもの</summary><div>

```python


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')),
])

```

</div></details>

<details><summary>Routerクラスを使用した場合</summary><div>

```python

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')),
]

```

</div></details>


明示化したコードを見てみるとわかるように、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クラスを定義し、独自のインスタンスを定義することもできる。
詳しくは[公式のドキュメントへ](https://www.django-rest-framework.org/api-guide/routers/)。




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

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



## 参考

[DRF公式](https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/)
[DRF公式 ViewSets](https://www.django-rest-framework.org/api-guide/viewsets/)
[DRF公式 Routerクラス](https://www.django-rest-framework.org/api-guide/routers/)