https://programming-gogogogo.hatenablog.com/entry/2023/03/21/063245
上記の記事の続きです。
前回の記事でエンドポイントURLとviewの関数を紐づけることはできたので、今回はViewsetsを定義してエンドポイントと紐づけてみたいと思います。
公式ドキュメントはこのあたりに記載がありました。 https://www.django-rest-framework.org/api-guide/viewsets/
https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/
REST framework includes an abstraction for dealing with ViewSets, that allows the developer to concentrate on modeling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.
ViewSetsを規約に基づいてうまく活用することで効率的にAPIが構築できそうです。
ViewSetsを定義するファイルを作成してみます。
touch drfproject/blog/viewsets.py
blog/viewsets.pyに以下のようにUserViewSetを定義しました。
from rest_framework import viewsets from .models import User from .serializers import UserSerializer class UserViewSet(viewsets.ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer
viewsets.ModelViewSetを継承しているので、上記だけでCRUD処理が実装されていると思われます。
次にルーティングを定義します。
drfproject/drfproject/urls.pyを以下のように編集しました。
from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path('blog/', include('blog.urls')) ]
これでlocalhost:8000/blog/以降のURLをdrfproject/blog/urls.pyに定義すれば良いはず。
drfproject/blog/urls.pyを以下のように編集しました。
from django.urls import path from blog import views urlpatterns = [ path('users_list', views.users_list) ]
views.users_listは以下のように関数型のビューになっています。
from .serializers import UserSerializer from .models import User from django.http import JsonResponse def users_list(request): users = User.objects.all() serializer = UserSerializer(users, many=True) return JsonResponse(serializer.data, safe=False)
ひとまずこのルーティングが動作することを確認します。
python manage.py runserver
curl http://localhost:8000/blog/users_list [{"id": 1, "name": "testuser", "age": 10}]
ここまでのルーティングは正常に動作していることを確認しました。
次にUserViewSetとエンドポイントを紐づけるルーティングを定義してみます。 エラーになりそうですが、以下のようにUserViewSet.as_view()でpathの第二引数に指定してみました。
from django.urls import path from blog import views from .viewsets import UserViewSet urlpatterns = [ path('users_list', views.users_list), path('user_viewset', UserViewSet.as_view()) ]
すると以下のエラーになりました。
raise TypeError("The `actions` argument must be provided when " TypeError: The `actions` argument must be provided when calling `.as_view()` on a ViewSet. For example `.as_view({'get': 'list'})`
DRFのソースコード読んでみると該当のエラーは以下だと思われます。 https://github.com/encode/django-rest-framework/blob/master/rest_framework/viewsets.py#L84
試しにas_viewに引数を追加すると一応サーバーは起動しました。
from django.urls import path from blog import views from .viewsets import UserViewSet urlpatterns = [ path('users_list', views.users_list), path('user_viewset', UserViewSet.as_view({ 'get': 'list' })) ]
リクエストも正常に返ってきました。
curl http://localhost:8000/blog/user_viewset [{"id":1,"name":"testuser","age":10}
しかし、これだとGETのリクエストしか受け付けなさそうな気がするのでPOSTのリクエストも投げてみたいです。
script/post.shを作成してcurlコマンドを実行するスクリプトを書きました。
mkdir script touch script/post.sh
script/post.sh
curl -X POST -H "Content-Type: application/json" -d '{"name":"testuser2", "age":"100"}' http://localhost:8000/blog/user_viewset
以下のコマンドでpost.shを実行するとPOSTメソッドは許可されていないエラーが発生しました。やっぱりそうですよねという感じです。
sh script/post.sh {"detail":"Method \"POST\" not allowed."}
CRUD処理ができるViewsetのルーティング方法を調査するにあたって、DRFのソースコードのコメントに良さそうな記載がありました。
https://github.com/encode/django-rest-framework/blob/master/rest_framework/viewsets.py#L11
Typically, rather than instantiate views from viewsets directly, you'll register the viewset with a router and let the URL conf be determined automatically. router = DefaultRouter() router.register(r'users', UserViewSet, 'user') urlpatterns = router.urls
viewsetsからviewをインスタンス化するよりも、routerにviewsetを登録しURL confに自動で決定させた方が良いとのこと。
また、公式ドキュメントでは以下のページにも説明がありました。 https://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/#using-routers
Because we're using ViewSet classes rather than View classes, we actually don't need to design the URL conf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using a Router class. All we need to do is register the appropriate view sets with a router, and let it do the rest.
ViewよりもViewSetの方を使うため、自身でURLを設定する必要はない。Routerクラスを使うことでviewとURLを自動で繋げることができるので、routerにviewsetを登録さえすれば良いっぽいです。
書かれているコードをそのまま真似してdrfproject/blog/urls.pyを修正しました。
from django.urls import path, include from blog import views from .viewsets import UserViewSet from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register(r'users_list', views.users_list, basename='users_list') router.register(r'user_viewset', UserViewSet, basename='user_viewset') urlpatterns = [ path('', include(router.urls)), ]
すると以下のエラーが出ました。
AttributeError: 'function' object has no attribute 'get_extra_actions'
そういえばusers_listはViewsetsではなく関数だったので、特定のURLを明示的に指定する必要がありそうです。
drfproject/blog/urls.pyを以下のように修正すると、サーバーが起動しました。
from django.urls import path, include from blog import views from .viewsets import UserViewSet from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register(r'user_viewset', UserViewSet, basename='user_viewset') urlpatterns = [ path('', include(router.urls)), path('users_list', views.users_list) ]
再度curlコマンドを実行してみます。
sh script/post.sh
すると以下のエラーになりました。
RuntimeError: You called this URL via POST, but the URL doesn't end in a slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while maintaining POST data. Change your form to point to localhost:8000/blog/user_viewset/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.
リクエストのURLの末尾のスラッシュがなかったためエラーになったようです。 script/post.shを修正しました。
curl -X POST -H "Content-Type: application/json" -d '{"name":"testuser2", "age":"100"}' http://localhost:8000/blog/user_viewset/
再度sh post.sh
を実行すると成功しました。ちなみに何回か同じコマンド実行したのでidは4になっています。
sh script/post.sh {"id":4,"name":"testuser2","age":100}
再度一覧取得してDBのデータを確認してみます。 これもスクリプト書いておきます。
touch script/list.sh
script/list.sh
curl -H "Content-Type: application/json" http://localhost:8000/blog/user_viewset/
一覧取得してみると、同じPOSTリクエストを続けて投げたので、同じ名前のユーザーが作成されてしまっています。
sh script/list.sh [{"id":1,"name":"testuser","age":10},{"id":2,"name":"testuser2","age":100},{"id":3,"name":"testuser2","age":100},{"id":4,"name":"testuser2","age":100}]
確かモデルの定義でフィールドをuniqueにできたはずなので試してみます。
drfproject/blog/models.py
from django.db import models class User(models.Model): name = models.CharField(max_length=100, unique=True) # uniqueを追加 age = models.IntegerField()
おそらくmigrationしないと変更が適用されないはずなので以下を実行します。
python manage.py makemigrations Migrations for 'blog': blog/migrations/0002_alter_user_name.py - Alter field name on user
python manage.py migrate
すると以下のエラーになりました。
django.db.utils.IntegrityError: UNIQUE constraint failed: new__blog_user.name
おそらくユニーク制約があるカラムですでに重複が発生していることが問題な気がするので、データを削除してみます。
どうせなので削除用のスクリプトも書いておきます。
touch script/delete.sh
script/delete.shは実行時にターミナルで引数を指定できるように$1で書いてあります。ここがUserモデルのIDになります。
curl -X DELETE -H "Content-Type: application/json" http://localhost:8000/blog/user_viewset/$1/
以下を実行してuser_nameが重複しているデータが消えたことを確認しました。
sh script/delete.sh 2 sh script/delete.sh 3 sh script/delete.sh 4 sh script/list.sh [{"id":1,"name":"testuser","age":10}]
再度migrateし、成功することを確認しました。
python manage.py migrate Operations to perform: Apply all migrations: admin, auth, blog, contenttypes, sessions Running migrations: Applying blog.0002_alter_user_name... OK
再度同じPOSTリクエストを送ると一度目は成功し、二度目に失敗したのでnameが重複するuserは登録できなくなっていることが確認できました。
sh script/post.sh {"id":2,"name":"testuser2","age":100} sh script/post.sh {"name":["user with this name already exists."]}