Django REST framework（DRF）公式チュートリアル⑥ ViewSets & Routers

# Tutorial 6: ViewSets & Routers

### はじめに
本記事は、Django REST framework の公式チュートリアル[Tutorial 6: ViewSets & Routers](https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/)の内容をもとに、和訳および補足解説を行ったものです。

基本的にはチュートリアルの翻訳を軸に構成しており、`✏️補足`の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。

他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇  
[DRF（Django REST framework）公式チュートリアルを日本語でまとめてみた【全6回】](https://qiita.com/yutty-kov/items/1850f3e865ec1a50a7bb)

それでは、本チュートリアルを始めましょう。
- - -

Django REST framework には <font color="#d14">`ViewSet`</font> を扱うための抽象化 が用意されており、開発者は API の状態や操作のモデリングに集中できるようになっています。一方で、URL の構築は一般的な規約に基づいて自動的に処理されます。

<font color="#d14">`ViewSet`</font> クラスは、<font color="#d14">`View`</font> クラスとほぼ同じですが、<font color="#d14">`get`</font> や <font color="#d14">`put`</font> といった HTTP メソッドのハンドラではなく、<font color="#d14">`retrieve`</font> や <font color="#d14">`update`</font> などの「操作(アクション)」を提供します。

<font color="#d14">`ViewSet`</font> クラスは、通常 <font color="#d14">`Router`</font> クラスを使ってビュー群に変換されるタイミングで初めて、各 HTTP メソッドのハンドラに関連付けられます。<font color="#d14">`Router`</font> クラスは、URL設定の定義に関する複雑な処理を自動的に行ってくれます。

### ViewSetを使ってリファクタリングする
現在の複数のViewを、ViewSet を使う形にリファクタリングしていきましょう。

まず最初に、<font color="#d14">`UserList`</font> クラスと <font color="#d14">`UserDetail`</font> クラスを 1 つの <font color="#d14">`UserViewSet`</font> クラスにまとめてリファクタリングします。
<font color="#d14">`snippets/views.py`</font> ファイル内で、これら 2 つのビュークラスを削除し、代わりに 1 つの ViewSet クラスを定義します：

```python:snippets/views.py
from rest_framework import viewsets


class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    このViewSetは自動的に list と retrieve の操作を提供します。
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer
```

ここでは、<font color="#d14">`ReadOnlyModelViewSet`</font> クラスを使用して、デフォルトの「読み取り専用」操作を自動的に提供しています。
通常のViewを使っていたときと同様に、<font color="#d14">`queryset`</font> 属性と <font color="#d14">`serializer_class`</font> 属性は引き続き設定していますが、もはやこれらの情報を別々の2つのクラスに重複して指定する必要はありません。

次に、<font color="#d14">`SnippetList`</font>、<font color="#d14">`SnippetDetail`</font>、<font color="#d14">`SnippetHighlight`</font> の3つのビュークラスを置き換えます。
この3つのビューを削除し、再び1つのクラスに置き換えることができます。

```python:snippets/views.py
from rest_framework import permissions
from rest_framework import renderers
from rest_framework.decorators import action
from rest_framework.response import Response


class SnippetViewSet(viewsets.ModelViewSet):
    """
    この ViewSet は、自動的に list、create、retrieve、update、および destroy の各操作を提供します。

    さらに、追加で highlight 操作も提供しています。
    """
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

    @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)
```

今回は、読み取りおよび書き込みの標準操作をすべて利用できるようにするために、<font color="#d14">`ModelViewSet`</font> クラスを使用しました。

また、<font color="#d14">`@action`</font> デコレーターを使って <font color="#d14">`highlight`</font> という名前のカスタムアクションを作成していることにも注目してください。このデコレーターは、標準的な <font color="#d14">`create`</font>/<font color="#d14">`update`</font>/<font color="#d14">`delete`</font> の形式に当てはまらない独自のエンドポイントを追加する際に使用できます。

<font color="#d14">`@action`</font> デコレーターを使ったカスタムアクションは、デフォルトで GET リクエストに応答します。<font color="#d14">`POST`</font> リクエストに応答するアクションを作りたい場合は、<font color="#d14">`methods`</font> 引数を使うことができます。

カスタムアクションの URL は、デフォルトではメソッド名に基づいて構築されます。URL の構造を変更したい場合は、デコレーターのキーワード引数として <font color="#d14">`url_path`</font> を指定できます。

## ✏️補足
このセクションでは、`リファクタリング前` のコードを `リファクタリング後` に変更しましたね。

```python:
# リファクタリング前
class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

# リファクタリング後
class UserViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
```

ユーザーのリスト（一覧）データ と 詳細データを処理するクラスの両方にまとめて `queryset` と `serializer_class` を設定することができるようになりました。
なぜ、ユーザーのリスト（一覧）データ と 詳細データを処理するクラスをまとめることができるかというと、それは**ReadOnlyModelViewSetクラスがURLに応じてパターンを判別できるから**です。

以下のようにReadOnlyModelViewSetクラスは、「①URLに `<pk>` が含まれているか」 「②HTTPメソッドは何か」 の情報から対応する操作を自動判別してくれます。

| URL | HTTPメソッド | 対応する操作 |
|:-:|:-:|:-:|
| `/users/` | GET | list (全件取得)|
| `/users/<pk>/` | GET | 	retrieve (1件取得)|

<br>

また、`SnippetList`、`SnippetDetail`、`SnippetHighlight` の3つのビュークラスも `SnippetViewSet` ビュークラスに置き換わりました。

```python:
# リファクタリング前
class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]
    
class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = [renderers.StaticHTMLRenderer]

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

# リファクタリング後
class SnippetViewSet(viewsets.ModelViewSet):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

    @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)
```

ここで、Userビューと異なる箇所は、`highlight` クラスの存在です。

>また、<font color="#d14">`@action`</font> デコレーターを使って <font color="#d14">`highlight`</font> という名前のカスタムアクションを作成していることにも注目してください。このデコレーターは、標準的な <font color="#d14">`create`</font>/<font color="#d14">`update`</font>/<font color="#d14">`delete`</font> の形式に当てはまらない独自のエンドポイントを追加する際に使用できます。

上記のチュートリアルの説明にあった通り、`highlight` メソッドは標準の `list` / `retrieve` / `create` などのアクションに当てはまらない処理のため、カスタムのアクションを追加する `@action` デコレーターを使います。

この `@action` デコレーターを設定した `highlight()` 関数が、以下のようなURLの名称の一部としてそのまま使用されるため、このURLにアクセスした時に `highlight()` 関数が動きます。

```
/snippets/3/highlight/
```
<br>

また以下の `renderer_classes` と `detail` 引数にも軽く触れたいと思います。

```python:
@action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
```

- `renderer_classes`：レスポンスのフォーマットを指定
- `detail`：個別オブジェクトに対するアクションなのか リストオブジェクトに対するアクションなのか を指定
    - `detail=True`：個別オブジェクトに対するアクション
    　例）`/snippets/<pk>/highlight/`
    - `detail=False`：リストオブジェクトに対するアクション
    　例）`/snippets/highlight/`

- - -

### ViewSet を URL に明示的にバインドする
ハンドラメソッドは、URL設定を定義したときに初めて各操作（list や retrieveなど）にバインドされます。
この仕組みの裏側で何が起きているのかを確認するために、まず ViewSet から明示的に View のセットを作成してみましょう。

<font color="#d14">`snippets/urls.py`</font> ファイル内で、<font color="#d14">`ViewSet`</font> クラスを具体的なビューにバインドします。

```python:snippets/urls.py
from rest_framework import renderers

from snippets.views import api_root, SnippetViewSet, UserViewSet

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'
})
```

各 <font color="#d14">`ViewSet`</font> クラスから複数の View を作成していることに注目してください。これは、各 View に対して必要な各操作（listやcreateなど）に HTTP メソッドをバインドすることで実現しています。

リソースを具体的な View にバインドできたので、これらの View を通常通り URL 設定に登録できます。

```python:snippets/urls.py
urlpatterns = format_suffix_patterns([
    path('', api_root),
    path('snippets/', snippet_list, name='snippet-list'),
    path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
    path('users/', user_list, name='user-list'),
    path('users/<int:pk>/', user_detail, name='user-detail')
])
```

### ルーターを使う
<font color="#d14">`View`</font> クラスではなく <font color="#d14">`ViewSet`</font> クラスを使っているため、実は自分で URL 設定を設計する必要はありません。リソースを View や URL に接続するための規約は、<font color="#d14">`Router`</font> クラスを使えば自動的に処理できます。
必要なのは、該当する ViewSet をルーターに登録することだけで、あとはルーターが処理してくれます。

以下は、再構成された <font color="#d14">`snippets/urls.py`</font> ファイルです。

```python:snippets/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter

from snippets import views

# ルーターを作成し、ViewSet をそのルーターに登録します。
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet, basename='snippet')
router.register(r'users', views.UserViewSet, basename='user')

# これにより、APIのURLはルーターによって自動的に決定されます。
urlpatterns = [
    path('', include(router.urls)),
]
```

ViewSet をルーターに登録するのは、URLパターンを定義するのと似ています。
View用のURLプレフィックスと、ViewSet 自体の2つの引数を指定します。

使用している <font color="#d14">`DefaultRouter`</font> クラスは、API のルートビューも自動で作成してくれるため、<font color="#d14">`views`</font> モジュール内の <font color="#d14">`api_root`</font> 関数は削除してかまいません。


### View と ViewSet のトレードオフ
ViewSet を使うことは非常に便利な抽象化の方法です。
URL の命名規則を API 全体で一貫させやすくなり、記述すべきコードの量を減らすことができ、URL 設定の細かい部分に気を取られずに、API が提供する操作や表現に集中できます。

ただし、それが常に正しいアプローチであるとは限りません。
これは、関数ベースのViewの代わりにクラスベースのViewを使うときと同じように、考慮すべきトレードオフがあります。
ViewSet を使うと、個別に API ビューを構築するよりも処理の流れが明示的ではなくなります。