##はじめに
rayaと申します。
プログラミング初心者であるため、内容に誤りがあるかもしれません。
もし、誤りがあれば修正するのでどんどん指摘してください。
##今回の内容
DjangoのREST frameworkで学んだことをまとめてみました。
##DjangoのREST frameworkとは
・Django REST framework
Djangoと組み合わせて、簡単にAPIを作るための設計ルール
・API
外部から呼び出せる機能の塊。
Google mapのAPIを自分のサイトに組み込めば自分のサイトに地図を読み込める
##準備
詳細は公式サイトをご覧ください。
djangoがインストールしてある仮想環境にRest frameworkをインストールします。
pip install djangorestframework
以下のように、setting.pyファイルにRest frameworkを使用することを伝えます。今回はtutorial
プロジェクトの中にsnippetes
アプリケーションを作成します。
INSTALLED_APPS = [
...
'rest_framework',
'snippets.apps.SnippetsConfig',
]
##Serialization
snippets/serializer.pyにSerializerを作成します。
Serializerとは、DjangoのFormに似た働きをし、Modelに対してデータを適切な形にして入力、出力するものです。
Serializerにはシリアライズとデシリアライズという働きがあり、シリアライズとは、オブジェクトからデータを読み出すことで、デシリアライズとは、データを元にオブジェクトを作成したり、データを適切な形に変換することです。
models.pyにSnippetモデルを作成した上で、SnippetSerializerを以下のように作成します。
class SnippetSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(required=False, allow_blank=True, max_length=100)
code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False)
serializers.Serializer
はSerializerの1つで、オブジェクトならばなんでもシリアライズできます。
Fieldの引数に指定されているのはCommon argsで、SerializerのFieldに対して指定でき、以下のようなものがあります。
・read_only
読み込み(出力)専用のフィールドかどうかを指定します。Trueなら読み込みのみ可能です。
・required = False
値を入力しなければならないか否かを指定します。デフォルトだとTrueです。
{'base_template': 'textarea.html'}
はDjango Formのwidget=widgets.Textarea
に相当します。
widgetはDjangoでHTMLの要素を表現する方法で、Textareaなら<textarea></textarea>を表現しています。
上記のSnippetSerializer
クラス内に以下のメソッドを定義します。
これらのメソッドはそれぞれ、serializer.save()
が呼び出された時にインスタンスを作成、更新します(デシリアライズします)。
それぞれの使い分けはメソッド定義の形を見ればわかります。
第一引数が指定されていればupdate
メソッドが、指定されていなければcreate
メソッドが実行されます。
update
メソッドの定義にはinstance
引数が指定されており、これが第1引数になります。
def create(self, validated_data):
return Snippet.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
...
instance.save()
return instance
validated_data
には、バリデーションを通過したデータが格納されます。
バリデーションの通過は、shell上で確認でき、適切なデータだったらTrueになり、validated_data
に格納されます。
snippet = Snippet(code='print("hello, world")\n')
snippet.save()
serializer = SnippetSerializer(snippet)
serializer.is_valid()
# True #適切なデータだったらTrueになり、validated_dataに格納される
次は、先ほどとは異なるserializers.ModelSerializer
を見てみます。
このSerializerでも同様なことがより簡単にできます。
これはModelに紐づいたFieldを自動でSerializerのFieldに定義してくれます。
DjangoのModelFormに似ています。
serializers.Serializer
はどんなオブジェクトもシリアライズできましたが、serializers.ModelSerializer
はDjangoモデルのみシリアライズできます。
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ['id', 'title', 'code', 'linenos']
これだけで、SnippetモデルのFieldに基づいたSerializerを作成できます。
##Requests and Responses
REST frameworkにはいくつか中核を担う機能があります。
・Request objects
DjangoのHttpRequestを拡張し、より柔軟になった機能です。
request.data
属性により、POST、PUT、PATCHで受け取ったデータを処理できます。
・Response objects
return Response(data)でリクエストに応じて適切な形でデータを出力します。
・Status codes
数値のHTTPステータスコードではなく、HTTP_400_BAD_REQUEST
のような、そのステータスコードの意味を捉えやすい識別子を提供しています。
・Wrapping API views
API Viewの作成に使える2種類のラッパーがあります。
-
@api_view
: デコレーター。関数ベースのViewの前に記述する。 -
APIView
: クラス。クラスベースのViewに継承する。
これらはViewに対してRequest
インスタンスを受け取り、Response
オブジェクトに最適なコンテキストを追加します。
という訳で、これらを使ってViewを作ってみます。
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST'])
def snippet_list(request):
if request.method == 'GET':
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
status
はResponse
のStatus codesを示します。
CREATED, REQUESTといった識別子が記述されていて、意味が明快です。
request.data
やResponse
の記述でわかるように、何というかざっくり記述できるようになりました。
Response
が柔軟にデータを出力するようにできたことで、URLに色々な指定ができます。
まず、Viewにformat
引数を指定します。
このformat
引数に指定した値を接尾子とするURLが提供されるようになります。
.json
や.html
をURLに指定できます。
ここではNoneなので特に指定していません。
def snippet_list(request, format=None):
次にsnippets/urls.pyファイルにformat_suffix_patterns
を追加します。
これにより先ほどのformat
引数の値で各Viewにより表示されるページのURLを指定できます。
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
urlpatterns = [
path('snippets/', views.snippet_list),
path('snippets/<int:pk>', views.snippet_detail),
]
urlpatterns = format_suffix_patterns(urlpatterns)
##Class-based Views
ここまでは関数ベースのViewを使ってきましたが、クラスベースのViewに置き換えてみます。
views.pyを以下のようにします。
@api_view
デコレーターではなく、APIView
クラスを使っています。
クラスベースのViewでは、HTTPメソッドをそのままクラスのインスタンスメソッドとして設定します。
このインスタンスメソッドはハンドラメソッドと呼ばれます。
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
class SnippetList(APIView):
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
クラスベースのViewを使う場合は、urls.pyのViewの記述をviews.クラス名.as_view()
とします。
urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]
さらに簡潔にするためにMixinが使えます。
Mixinとは機能をまとめたクラスのことです。
単体では動作しないため他のクラスに継承して機能を提供する働きをします。
DjangoのREST frameworkのMixinには、作成・取得・更新・削除といった基本的な機能が実装されているので、Mixinを使うことでこれらの機能を簡単に呼び出すことができます。
まず、GenericAPIView
でListModelMixin
とCreateModelMixin
を追加します。
このGeneric〜に関しては後述します。
これらのMixinクラスにより.list()
と.create()
が使えるようになります。
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics
class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
*args
: tupleで複数の値をとる引数
**kwargs
: dictionaryでkeyとvalue2つの値をとる引数
実はさらに簡潔に記述できます。
REST frameworkにはデフォルトでGenericviewが導入されています。
GenericViewは先ほどのGenericAPIViewを基底クラスとし、Mixinを継承しています。
GenericViewクラスに対応した特定のクラスを継承させることで、それに紐づいたアクションメソッドを提供します。
アクションメソッドとはMixinで提供されるハンドラメソッド(上述)のようなものです。
先ほどのViewは以下のように記述できます。
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
##Authentication & Permissions
現状、このAPIは誰もがデータを編集、削除できます。
よって、以下の機能を実装します。
・スニペットとその作成者が関連付けられる。
・認証されたユーザーのみスニペットを作成できる。
・スニペットの作成者のみがそれを更新・削除できる。
・認証されていないリクエストは読み取りのみになる
まず、Userモデルに対する設定をしたいのでUserモデルのSerializerを作ります。
ModelSerializer
のところで行ったようにUserモデルと関連づけてSerializerを作成します。
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'snippets']
また、GenericViewを使い、Viewを設定します。
from django.contrib.auth.models import User
from snippets.serializers import UserSerializer
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
snippets/urls.pyにもURLを追加します。
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
ここからSnippetモデルとUserモデルを関連付けます。
models.pyのSnippetモデルに以下のFieldを追加します。
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
さらにSnippetList
Viewクラスに以下のメソッドを追加します。
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
これで.perform_create()
メソッドによりリクエストのあったユーザーの情報をowner
に代入し保存できます。
先ほどはmodels.pyにownerFieldを追加しましたが、次はserializers.pyのSnippetSerializerにownerフィールドを追加します。
この時、class Meta : にもownerを追加します。
owner = serializers.ReadOnlyField(source='owner.username')
ここで、source
引数は、Fieldの引数に指定され、そのFieldに入るシリアライズされたデータの値に属性を指定できます。
この場合は、owner
Fieldにownerのusernameが入ります。
ReadOnlyFieldはserializerに設定できるフィールドで読み取り専用であることを示します。
次は、認証されたユーザーのみがスニペットを作成し、それらを更新・削除できるようにします。
そのためにViewにPermissionを追加します。
views.pyにPermissionクラスをインポートし、Permissionを追加したいViewにそのPermissionクラスを指定します。
from rest_framework import permissions
#SnippetList Viewに追記
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
IsAuthenticatedOrReadOnly
は認証されたリクエストを受け取った場合、読み取りと書き込み権限を与え、認証されなければ読み取り権限しか与えないというPermissionです。
ユーザーを認証するるためにはログイン、ログアウト機能を実装し、ユーザー名とパスワードを送ってもらいます。
まず、tutorial/urls.pyに以下の内容を記述します。
from django.conf.urls import include
#ファイルの最後に記述
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
'api-auth/'
は任意の値で大丈夫です。
これだけでログイン、ログアウト機能が実装できます。
ここまでは、認証されれば(ログインできれば)、他のユーザーのスニペットを含め、全てのスニペットを編集できるように設定しています。
しかし、スニペットの編集は自分が作成したもののみにしたいですよね。
そこで、次は各オブジェクトに対するパーミッションを設定し、自分の投稿したオブジェクトのみ編集ができるようにします。
そのためにはCustom Permissionを実装する必要があるので、snippet/permissions.pyファイルを作成して以下のように記述します。
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
Basepermission
とhas_object_permission
によってCustom Permissionが実装され、そのオブジェクトの作成者のみ編集権限を得られるようになります。
has_object_permission(self, request, view, obj)
メソッドでは、permissionが設定されたViewかつ特定のオブジェクトに対して権限のあるユーザーからのアクセスと保証されるならTrueを返します。
同じような機能のhas_permission(self, request, view)
メソッドでは、permissionが設定されたView全てで呼び出され、権限があるユーザーからのリクエストならばTrueを返しますが、オブジェクトを指定しません。
SAFE_METHODS
は、GET, OPTIONS, HEAD
を含むタプルです。
要は読み取りのパーミッションを示しています。
よって、if request.method in permissions.SAFE_METHODS
以降は、読み取りのリクエストの場合常にTrueになり、読み取りだけなら常に許可します。
それ以外のリクエスト(書き込み)ならば、return obj.owner == request.user
としてオブジェクトの所有者とリクエストを送ったユーザーが同じならば書き込みを許可します。
このCustom Permissionをviews.pyに設定します。
from snippets.permissions import IsOwnerOrReadOnly
#SnippetDetailクラスに追記(この記事では表記していないので公式サイトでご確認ください)
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]
##Relationships & HyperlinkedAPIs
今のところ、usersとsnippetへのエンドポイントはありますが、API自身のエントリポイントがありません。
そこで、snippet/views.pyに以下の内容を追記してエントリポイントを作ります。
※エントリポイントとエンドポイントとは共に特定のリソースに与えられた一意なURI(web上の住所(URL)とweb上での名前(URN)の総称)で、同じ場所を示します。
その違いは、そのURIにアクセスする視点です。
サービスを提供する側からの視点に立つと、そのURIはプログラムを始める地点という意味でエントリポイントになり、サービスを利用する側からの視点に立つと、そのURIはアクセスさえすればあとはAPIが勝手にプログラムを開始し終了させてくれるという意味でエンドポイントになります。
例えば、Google mapのAPIを自分のサイトで使いたくて、あるURIにアクセスする場合、Google mapから見ればそのURIはエントリポイント、自分から見るとエンドポイントになります。
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
@api_view(['GET'])
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})
ここで、reverse関数は、URLを完全な形にします。
例えば、/foobar
ではなくhttp://example.com/foobar
で返します。
次に入力した内容のhtml表現をハイライトするコードを導入します。
Restframeworkが提供しているHTML Rendererには2種類あります。
1つは、TemplateHTMLRenderer
で、templateに基づいたHTMLをレンダリングします。
もう1つはStaticHTMLRenderer
で、あらかじめレンダリングされたHTMLを処理します。
今回は後者を扱います。
StaticHTMLRenderer
を使用する際は、APIがこれまでと異なる挙動をするので、GenericViewのメソッドを使わずに、別でViewのメソッドを定義する必要があります。
今回はsnippets/views.pyにget()メソッドを定義していきます。
from rest_framework import renderers
from rest_framework.response import Response
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)
snippets/urls.pyにここまでに定義したViewのURLを追記します。
path('', views.api_root),
path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view()),
ここからは、HyperLinkedModelSerializer
を使って、entity間をHyperlinkで繋げます。
entityとは、テキストにリンクをつけるための情報です。
HyperLinkedModelSerializer
はModelSerializer
とはidフィールドがデフォルトで存在しない点、HyperlinkedIdentityField
を用いたurlフイールドが存在する点、HyperLinkedRelatedField
でentity間を繋げる点が異なります。
snippets/serializers.pyに以下の内容を追記します。
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
model = Snippet
fields = ['url', 'id', 'highlight', 'owner',
'title', 'code', 'linenos']
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)
##SnippetDetailはこの記事で表記していないので公式サイトでご確認ください
class Meta:
model = User
fields = ['url', 'id', 'username', 'snippets']
ここで新たに定義されているhighlight
Fieldにはformat
引数でhtml
を指定する必要がある点は注意してください。
view_name
にはurls.pyで設定したURLの名前が入ります。
UserとSnippetモデルは大量のオブジェクトを返しています。
そこで、1つのページあたりに表示できるオブジェクトの数を制限します。
このことをPaginationと言います。
tutorial/settings.pyに以下の内容を追記します。
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
##Viewsets & Routers
REST frameworkにはURLの構築を自動で行ってくれるViewsetという仕組みがあります。
Viewsetクラスは、Viewクラスとほとんど同じですが、GET,PUTなどのHTTPメソッドではなく、read,updateなどのCRUDでデータをサーバーとやりとりします。
※厳密には、Viewsetクラスがインスタンス化される瞬間にHTTPメソッドは使われます。(後述)
ここまで作ってきたViewをViewsetに置き換えます。
UserListクラスとUserDetailクラスを1つのUserViewsetクラスにまとめます。
ReadOnlyModelViewSet
クラスによって「読み取り機能」が設定され、list, detailというアクションが自動で定義されます。
from rest_framework import viewsets
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
次は、SnippetList
,SnippetDetail
, SnippetHighlight
クラスをSnippetViewset
にまとめます。
from rest_framework.decorators import action
from rest_framework.response import Response
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)
ここではModelViewset
によって「読み書き機能」が設定され、list, create, retrieve, update, destroyが自動で定義されます。
また、CRUD以外のアクションを追加したい時は、@action
の後に記述でき、highlight
, perform_create
アクションが定義されています。
一応、これらのアクションにはGETメソッドがデフォルトで割り当てられます。
他のHTTPメソッドにも対応させたい場合は、method
引数で指定できます。
※detail
引数について間違ってたらコメントお願いします!
これはかなり自分なりの解釈ですが、
detail=
はTrueならpkの設定が必要になり、データ1つあたりに対してアクションを起こしたい場合に使用され、Falseの場合はpkが不要でデータ一覧に対してアクションを起こしたい場合に使用されます。
次にViewsetとURLを繋げます。
具体的には、Viewsetの各アクションをHTTPメソッドへどう割り当てるかをsnippets/urls.pyで定めます。
from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers
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'
})
その後、URLconf(tutorial/urls.py)で実際に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')
])
しかし、最初に言ったように、実際はViewsetクラスを使っているため、自分でURLを設定する必要はありません。
それはRouterクラスが自動で行ってくれます。
Routerクラスが機能するようにsnippets/urls.pyを書き換えると以下のようになります。
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views
#Routerクラスを作り、Viewsetクラスを登録する
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)
#URLが自動で割り当てられる
urlpatterns = [
path('', include(router.urls)),
]
DefaultRouter
クラスは自動でAPIルートビューを作るので、Relationships & HyperlinkedAPIsのところで作成したViewsモジュールからapi_root
メソッドを削除できます。
##メモ
Tutorialの中で気になって調べたことを一部まとめています。
メモ程度に書いているのでご了承ください。
・参照と逆参照
class Food(models.Model):
name = models.CharField(max_length=100)
class Person(models.Model):
name = models.CharField(max_length=100)
favorite_food = models.ForeignKey(Food, on_delete="CASCADE", null=True)
foreignkeyフィールドを設定してあるモデルからその関連するモデルのカラムを取得するときは参照。
person1 = Person.objects.get(name="ジョブス")
person1.favorite_food
インスタンス.フィールド名
foreignkeyフィールドの設定してない方のモデルからそれち関連したforeignkeyフィールドの設定したあるモデルのフォールドを参照する場合は逆参照
food1 = Food.objects.get(name="寿司")
food1.person_set.all()
インスタンス.クラス名_set
・repr(serializer)
str()と似ている。
・from django.http import HttpResponse, JsonResponse
辞書型のオブジェクトをJsonreaponseに渡し、returnするとjsonで送信できる
・from django.views.decorators.csrf import csrf_exempt
csrf_tokenを渡していないPOSTメソッドはエラーになるが、それを無効化する。関数の上に@csrf_exempt
を記述
・from rest_framework.parsers import JSONParser
parserは、構文を解析するプログラム。
・many=True
引数にmany=Trueを指定すると複数のデータをまとめて扱えるようになる。
・safe=False
文字列に対してエスケープが必要でないと示す
・Pastebin
保存したデータを公開できるウェブアプリケーションのサービス。
##参考
・公式サイト(https://www.django-rest-framework.org)
・https://qiita.com/ryoo17/items/326f8adda8423abf1ce4
・https://note.crohaco.net/2018/django-rest-framework-serializer/
・http://sandmark.hateblo.jp/entry/2017/09/30/160945
・https://qiita.com/NagaokaKenichi/items/6298eb8960570c7ad2e9
・https://qiita.com/kimihiro_n/items/86e0a9e619720e57ecd8