Tutorial 6: ViewSets & Routers
はじめに
本記事は、Django REST framework の公式チュートリアルTutorial 6: ViewSets & Routersの内容をもとに、和訳および補足解説を行ったものです。
基本的にはチュートリアルの翻訳を軸に構成しており、✏️補足の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。
他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇
DRF(Django REST framework)公式チュートリアルを日本語でまとめてみた【全6回】
それでは、本チュートリアルを始めましょう。
Django REST framework には ViewSet を扱うための抽象化 が用意されており、開発者は API の状態や操作のモデリングに集中できるようになっています。一方で、URL の構築は一般的な規約に基づいて自動的に処理されます。
ViewSet クラスは、View クラスとほぼ同じですが、get や put といった HTTP メソッドのハンドラではなく、retrieve や update などの「操作(アクション)」を提供します。
ViewSet クラスは、通常 Router クラスを使ってビュー群に変換されるタイミングで初めて、各 HTTP メソッドのハンドラに関連付けられます。Router クラスは、URL設定の定義に関する複雑な処理を自動的に行ってくれます。
ViewSetを使ってリファクタリングする
現在の複数のViewを、ViewSet を使う形にリファクタリングしていきましょう。
まず最初に、UserList クラスと UserDetail クラスを 1 つの UserViewSet クラスにまとめてリファクタリングします。
snippets/views.py ファイル内で、これら 2 つのビュークラスを削除し、代わりに 1 つの ViewSet クラスを定義します:
from rest_framework import viewsets
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
このViewSetは自動的に list と retrieve の操作を提供します。
"""
queryset = User.objects.all()
serializer_class = UserSerializer
ここでは、ReadOnlyModelViewSet クラスを使用して、デフォルトの「読み取り専用」操作を自動的に提供しています。
通常のViewを使っていたときと同様に、queryset 属性と serializer_class 属性は引き続き設定していますが、もはやこれらの情報を別々の2つのクラスに重複して指定する必要はありません。
次に、SnippetList、SnippetDetail、SnippetHighlight の3つのビュークラスを置き換えます。
この3つのビューを削除し、再び1つのクラスに置き換えることができます。
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)
今回は、読み取りおよび書き込みの標準操作をすべて利用できるようにするために、ModelViewSet クラスを使用しました。
また、@action デコレーターを使って highlight という名前のカスタムアクションを作成していることにも注目してください。このデコレーターは、標準的な create/update/delete の形式に当てはまらない独自のエンドポイントを追加する際に使用できます。
@action デコレーターを使ったカスタムアクションは、デフォルトで GET リクエストに応答します。POST リクエストに応答するアクションを作りたい場合は、methods 引数を使うことができます。
カスタムアクションの URL は、デフォルトではメソッド名に基づいて構築されます。URL の構造を変更したい場合は、デコレーターのキーワード引数として url_path を指定できます。
✏️補足
このセクションでは、リファクタリング前 のコードを リファクタリング後 に変更しましたね。
# リファクタリング前
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件取得) |
また、SnippetList、SnippetDetail、SnippetHighlight の3つのビュークラスも SnippetViewSet ビュークラスに置き換わりました。
# リファクタリング前
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 クラスの存在です。
また、
@actionデコレーターを使ってhighlightという名前のカスタムアクションを作成していることにも注目してください。このデコレーターは、標準的なcreate/update/deleteの形式に当てはまらない独自のエンドポイントを追加する際に使用できます。
上記のチュートリアルの説明にあった通り、highlight メソッドは標準の list / retrieve / create などのアクションに当てはまらない処理のため、カスタムのアクションを追加する @action デコレーターを使います。
この @action デコレーターを設定した highlight() 関数が、以下のようなURLの名称の一部としてそのまま使用されるため、このURLにアクセスした時に highlight() 関数が動きます。
/snippets/3/highlight/
また以下の renderer_classes と detail 引数にも軽く触れたいと思います。
@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 のセットを作成してみましょう。
snippets/urls.py ファイル内で、ViewSet クラスを具体的なビューにバインドします。
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'
})
各 ViewSet クラスから複数の View を作成していることに注目してください。これは、各 View に対して必要な各操作(listやcreateなど)に HTTP メソッドをバインドすることで実現しています。
リソースを具体的な View にバインドできたので、これらの View を通常通り URL 設定に登録できます。
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')
])
ルーターを使う
View クラスではなく ViewSet クラスを使っているため、実は自分で URL 設定を設計する必要はありません。リソースを View や URL に接続するための規約は、Router クラスを使えば自動的に処理できます。
必要なのは、該当する ViewSet をルーターに登録することだけで、あとはルーターが処理してくれます。
以下は、再構成された 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つの引数を指定します。
使用している DefaultRouter クラスは、API のルートビューも自動で作成してくれるため、views モジュール内の api_root 関数は削除してかまいません。
View と ViewSet のトレードオフ
ViewSet を使うことは非常に便利な抽象化の方法です。
URL の命名規則を API 全体で一貫させやすくなり、記述すべきコードの量を減らすことができ、URL 設定の細かい部分に気を取られずに、API が提供する操作や表現に集中できます。
ただし、それが常に正しいアプローチであるとは限りません。
これは、関数ベースのViewの代わりにクラスベースのViewを使うときと同じように、考慮すべきトレードオフがあります。
ViewSet を使うと、個別に API ビューを構築するよりも処理の流れが明示的ではなくなります。