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使えると判断したときにスムーズに扱えるようになるようにというのが私達初学者の目指すところなのだと感じた。