Django REST Frameworkの基本事項まとめ
最初に
別記事でアプリを実際に作成する前に調べた内容等をまとめました。
ハマったポイントもあるので同じ状況の人の参考になればと思います。
コード
初期構築
mkdir backend
cd backend/
django-admin startproject config .
python manage.py startapp todo
python manage.py startapp apiv1
config/settings.pyの編集
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party apps
'rest_framework',
# my apps
'apiv1.apps.Apiv1Config',
'todo.apps.TodoConfig',
]
...
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
基本的なモデル
データベース内に存在する複数のオブジェクトを区別するために、primary keyを設定する必要があります。
何も設定しない場合は連続的に付番されるidが自動で作成されますが、悪意ある人に悪利用されないためにUUIDで設定します。
また、フィールドに付与したオプションにより後のシリアライザが実行するバリデーションなどが自動で決まっていきます。
モデル全体に対するオプションの詳細はこちらを参考に。
フィールドに関する詳細はこちら
import uuid
from django.db import models
class Task(models.Model):
""" Task model
id : primary key
title : task name
importance : importance of task
"""
class Meta:
db_table = 'task'
ordering = ['created_at',]
verbose_name = verbose_name_plural = 'タスク'
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(verbose_name='Title', max_length=20, unique=True)
content = models.CharField(verbose_name='content', max_length=120, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
マイグレーション
アプリケーションのモデルの変更差分からマイグレーションファイルを作成します。
その後、そのマイグレーションファイルを元にDBに反映されます。
python manage.py makemigrations todo
python manage.py migrate todo
管理サイトへの登録
管理サイトで登録・編集ができるようにadmin.py
を修正します。
from django.contrib import admin
from .models import Task
# Register your models here.
class TaskModelAdmin(admin.ModelAdmin):
list_display = ('title', 'content', 'created_at', 'updated_at',)
ordering = ('-created_at',)
readonly_fields = ('id', 'created_at', 'updated_at', )
admin.site.register(Task, TaskModelAdmin)
動作確認用にユーザーを作成します。(username: admin, pw: pass12345)
python manage.py createsuperuser
# 動作確認でアクセスしてみる
python manage.py runserver
# http://127.0.0.1:8000/admin/ にアクセス
django shellで動作確認
現状API実装できていないので簡単にshellで動作を見てみます。
実際に以下の作業を行うと、管理サイトでも追加されていることが確認できます。
python manage.py shell
from todo.models import Task
# 最初は空
Task.objects.all()
# 登録
task = Task(title="title1", content='content1')
task.save()
task = Task(title="title2", content='content2')
task.save()
Task.objects.all()
# フィルタ
Task.objects.filter(title='title1')
# 取得・削除
task = Task.objects.get(title='title1')
task.delete()
Task.objects.all()
シリアライザ
シリアリザは「入力データ(jsonデータ)とモデルオブジェクトの相互変換」「入力データのバリデーション」の2つの役割があります。
JSONの入出力構造がモデルのフィールド定義をベースにしたものになる場合、ModelSrializer
を継承します。
これを継承することでモデル定義に基づいたバリデーションを実施するため記述量が大幅に削減されます。
from rest_framework import serializers
from todo.models import Task
class TaskSerializer(serializers.ModelSerializer):
""" Serializer for Task Model """
class Meta:
# 対象のモデルクラスを指定
model = Task
# 利用するモデルのフィールドを指定
fields = ['id', 'title', 'content']
django shellで動作確認
現状API実装できていないので簡単にshellで動作を見てみます。
python manage.py shell
rom django.utils.six import BytesIO
from rest_framework.parsers import JSONParser
from rest_framework.renderers import JSONRenderer
from apiv1.serializers import TaskSerializer
from todo.models import Task
# json to object
data = JSONParser().parse(BytesIO('{"title": "title10", "content": "content10"}'.encode()))
serializer = TaskSerializer(data=data)
# validation
serializer.is_valid()
# 登録
serializer.save()
serializer.instance
# 一部更新
serializer.instance.id
task = Task.objects.get(pk='c1478697-aa3f-4627-b47f-653f9751a733')
serializer = TaskSerializer(instance=task, data={'title': 'title100'})
serializer.is_valid()
serializer.save()
serializer.instance
# object to json
task = Task.objects.get(pk='c1478697-aa3f-4627-b47f-653f9751a733')
task
serializer = TaskSerializer(instance=task)
serializer.data
JSONRenderer().render(serializer.data)
ビュー
ビューではリクエストオブジェクトを受け取り、レスポンスオブジェクトを作成して返します。
ビューのクラス内では、上のserializerを活用してpost
やget
を実装します。
上の内容が理解できていればやっていることを理解できるかと思います。
from django.shortcuts import get_object_or_404
from rest_framework import status, views
from rest_framework.response import Response
from todo.models import Task
from .serializers import TaskSerializer
class TaskCreateListAPIView(views.APIView):
""" Taskモデルの登録API """
def post(self, request, *args, **kwargs):
# Serializerを作成
serializer = TaskSerializer(data=request.data)
# バリデーション実行
serializer.is_valid(raise_exception=True)
# モデルオブジェクトを登録
serializer.save()
# JSON文字列をレスポンスとして返す
return Response(serializer.data, status.HTTP_201_CREATED)
def get(self, request, *args, **kwargs):
""" Taskモデルの一覧取得API """
# 複数のobjectの場合、many=Trueを指定します
serializer = TaskSerializer(instance=Task.objects.all(), many=True)
return Response(serializer.data, status.HTTP_200_OK)
class TaskRetrieveUpdataDestroyAPIView(views.APIView):
""" Taskモデルのpk APIクラス """
def get(self, request, pk, *args, **kwargs):
""" Taskモデルの詳細取得API """
# モデルオブジェクトを取得
task = get_object_or_404(Task, pk=pk)
serializer = TaskSerializer(instance=task)
return Response(serializer.data, status.HTTP_200_OK)
def put(self, request, pk, *args, **kwargs):
""" Taskモデルの更新API """
task = get_object_or_404(Task, pk=pk)
serializer = TaskSerializer(instance=task, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status.HTTP_200_OK)
def patch(self, request, pk, *args, **kwargs):
""" Taskモデルの更新API """
task = get_object_or_404(Task, pk=pk)
# partial=Trueにより、request.dataで指定したデータのみ更新される
serializer = TaskSerializer(instance=task, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status.HTTP_200_OK)
def delete(self, request, pk, *args, **kwargs):
""" Taskモデルの削除API """
task = get_object_or_404(Task, pk=pk)
task.delete()
return Response(status.HTTP_200_OK)
URLconf
config/urls.py
をまず変更します。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('apiv1.urls')),
]
apiv1/urls.py
も変更します。
from django.urls import path, include
from . import views
app_name = 'apiv1'
urlpatterns = [
path('tasks/', views.TaskCreateListAPIView.as_view()),
path('tasks/<pk>/', views.TaskRetrieveUpdataDestroyAPIView.as_view()),
]
python manage.py runserver
でテストサーバーを起動し、http://127.0.0.1:8000/api/v1/tasks/
にアクセスすると、動作確認ができます。
ここまでで単純なAPIを構築することができました。次のセクションからはより細かいケースを説明していきます。
モデルに依存しないシリアライザ
モデルに依存しない自由な形式のJSONを入出力するAPIを作成したい場合、serializers.Serializer
を継承したシリアライザを作成します。
シリアライザ
モデルが関係ない追加するフィールドをserializers.?Fieldで定義します。ここでは、birth_dateとblood_typeを入力にし、current_dateとfortuneを返すシリアライザを作成します。ここで、is_valid()後の出力時にget_current_date
とget_fortune
を呼ぶように、serializers.SerializerMethodFieldを使用しています。
import random
from django.utils import timezone
from rest_framework import serializers
class FortuneSerializer(serializers.Serializer):
""" 今日の運勢を返すためのシリアらいざ """
birth_date = serializers.DateField()
blood_type = serializers.ChoiceField(choices=['A', 'B', 'O', 'AB'])
# 出力時にget_current_date()が呼ばれる
current_date = serializers.SerializerMethodField()
# 出力時にget_fortune()が呼ばれる
fortune = serializers.SerializerMethodField()
def get_current_date(self, obj):
return timezone.localdate()
def get_fortune(self, obj):
seed = '{}{}{}'.format(
timezone.localdate(), obj['birth_date'], obj['blood_type']
)
random.seed(seed)
return random.choice(
['★☆☆', '★★☆', '★★★']
)
ビュー
ビューでは、ポストのみ実装しており、単純にデータを受け取った後にis_valid()を実施後のデータを返すように実装しています。
http://127.0.0.1:8000/api/v1/fortune/ にアクセス後、{"birth_date": "1990-01-01", "blood_type": "A"}をPOSTで送信すると運勢を見ることができます。
from .serializers import FortuneSerializer
class FortuneAPIView(views.APIView):
""" FortuneAPI """
def post(self, request, *args, **kwargs):
# Serializerを作成
serializer = FortuneSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# JSON文字列をレスポンスとして返す
return Response(serializer.data, status.HTTP_200_OK)
Foreign keyを使用したモデル
多対一の場合、以下のようにForeign keyを「多」側のテーブルに設けることが一般的です。
import uuid
from django.db import models
class Publisher(models.Model):
""" Publisher Model """
class Meta:
db_table = 'publisher'
ordering = ['created_at',]
verbose_name = verbose_name_plural = '出版社'
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(verbose_name="出版社名", max_length=30)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
class Book(models.Model):
""" Book Model """
class Meta:
db_table = 'book'
ordering = ['created_at',]
verbose_name = verbose_name_plural = '本'
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(verbose_name="タイトル", max_length=30)
price = models.IntegerField(verbose_name='価格', null=True)
publisher = models.ForeignKey(Publisher, verbose_name="出版社名", on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
django shellで動作確認
現状API実装できていないので簡単にshellで動作を見てみます。
Foreign keyの場合、ForeignKeyのフィールドを定義した側のクラスには、関連先のモデルオブジェクトのprime keyを扱うための「foreignkeyのfieldname_id」という属性が自動で付与されます。オブジェクト自体を扱うためには、ForeignKeyフィールドにアクセスします。逆に、もう一方側からアクセスするには、「クラス名_set」でアクセスができます。
python manage.py shell
from shop.models import Book, Publisher
# publisherを登録
publisher = Publisher(name='publisher100')
publisher.save()
# リレーションフィールドを合わせて登録
book = Book(title="title1000", price=1500, publisher=publisher)
book.save()
シリアライザ
こちらを参考にしてシリアライザを作成しました。
BookSerializerでは、publisherとpublisher_idが追加されていますが、これはモデルに依存しないシリアライザで見たように、モデルに関係ないフィールドを定義しています。特に、publisher_idはモデル定義にないので、Bookモデルオブジェクトを作る際の処理を変更しています。
class PublisherSerializer(serializers.ModelSerializer):
""" Serializer for Publisher Model """
class Meta:
model = Publisher
fields = ['id', 'name']
class BookSerializer(serializers.ModelSerializer):
""" Serializer for Book Model """
publisher = PublisherSerializer(read_only=True)
publisher_id = serializers.PrimaryKeyRelatedField(queryset=Publisher.objects.all(), write_only=True)
class Meta:
model = Book
# publisher_id: モデルには存在しない追加する新フィールド
fields = ['id', 'title', 'price', 'publisher', 'publisher_id']
def create(self, validated_date):
# Bookモデルオブジェクトを作る際の関数
# 作る際にモデル定義にないフィールド(publisher_id)があるので、publisherに置き換えてから削除する
validated_date['publisher'] = validated_date.get('publisher_id', None)
del validated_date['publisher_id']
return Book.objects.create(**validated_date)
ビュー
ビューは普通のモデルの時と同じように実装します。
from rest_framework import status, views, viewsets
from rest_framework.response import Response
from shop.models import Book, Publisher
from .serializers import BookSerializer, PublisherSerializer
class PublisherCreateListAPIView(views.APIView):
""" Publisherモデルの登録API """
def post(self, request, *args, **kwargs):
# Serializerを作成
serializer = PublisherSerializer(data=request.data)
# バリデーション実行
serializer.is_valid(raise_exception=True)
# モデルオブジェクトを登録
serializer.save()
# JSON文字列をレスポンスとして返す
return Response(serializer.data, status.HTTP_201_CREATED)
def get(self, request, *args, **kwargs):
""" Taskモデルの一覧取得API """
serializer = PublisherSerializer(instance=Publisher.objects.all(), many=True)
return Response(serializer.data, status.HTTP_200_OK)
class BookCreateListAPIView(views.APIView):
""" Bookモデルの登録API """
def post(self, request, *args, **kwargs):
# Serializerを作成
serializer = BookSerializer(data=request.data)
# バリデーション実行
serializer.is_valid(raise_exception=True)
# モデルオブジェクトを登録
serializer.save()
# JSON文字列をレスポンスとして返す
return Response(serializer.data, status.HTTP_201_CREATED)
def get(self, request, *args, **kwargs):
""" Taskモデルの一覧取得API """
serializer = BookSerializer(instance=Book.objects.all(), many=True)
return Response(serializer.data, status.HTTP_200_OK)
シリアライザにバリデーションを追加
シリアライザにバリデーションを追加する方法としては4通りあります。
それぞれ単体/複数のフィールド、実行順が異なります。
http://127.0.0.1:8000/api/v1/tasks/ にアクセスしてPOSTを実行してみると、validatorが機能していることが分かります。
from django.core.validators import RegexValidator
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from todo.models import Task
class TaskSerializer(serializers.ModelSerializer):
""" Serializer for Task Model """
class Meta:
# 対象のモデルクラスを指定
model = Task
# 利用するモデルのフィールドを指定
fields = ['id', 'title', 'content']
# validatorを追加(複数field可能)
validators = [
UniqueTogetherValidator(
queryset = Task.objects.all(),
fields=('title', 'content'),
message="titleとcontentでユニークになっている必要があります"
),
]
# 単体のfieldに対するvalidator
extra_kwargs = {
'title': {
'validators': [
RegexValidator(
r'^D.+$', message="titleは「D」から始めてください"
),
],
},
}
def validate_title(self, value):
""" 単体のフィールドに対するvalidator """
if 'Java' in value:
raise serializers.ValidationError("titleに「Java」を含めないでください")
return value
def validate(self, data):
""" 複数フィールドのvalidator """
title = data.get('title')
content = data.get('content')
if title and '#' in title and content and '#' in content:
raise serializers.ValidationError(
"titleとcontentの両方に「#」が含まれてはいけません"
)
return data
クエリ文字列で条件検索
django-filterというライブラリを使用することで簡単に導入できます。
「priceが?円以下」などより詳細なフィルタリングをする場合にも対応できるように、
以下のようにフィルタクラスを定義して使用しています。
実装できたら、http://127.0.0.1:8000/api/v1/book/?price__lte=1500 にアクセスし確認してみます。
from django_filters import rest_framework as filters
from rest_framework import status, views, viewsets
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from shop.models import Book
from .serializers import BookSerializer
class BookFilter(filters.FilterSet):
""" Bookモデル用のフィルタ """
# 「price__lte」というキーで「priceが?円以下」という条件でフィルタリング可能に
price__lte = filters.NumberFilter(field_name='price', lookup_expr='lte')
class Meta:
model = Book
# フィルタリング可能なfieldを指定
fields = '__all__'
class BookCreateListAPIView(views.APIView):
""" Bookモデルの登録API """
def post(self, request, *args, **kwargs):
# Serializerを作成
serializer = BookSerializer(data=request.data)
# バリデーション実行
serializer.is_valid(raise_exception=True)
# モデルオブジェクトを登録
serializer.save()
# JSON文字列をレスポンスとして返す
return Response(serializer.data, status.HTTP_201_CREATED)
def get(self, request, *args, **kwargs):
""" Taskモデルの一覧取得API """
# モデルオブジェクトをクエリ文字列を使ってフィルタリングした結果を取得
filterset = BookFilter(request.query_params, queryset=Book.objects.all())
if not filterset.is_valid():
raise ValidationError(filterset.errors)
serializer = BookSerializer(instance=filterset.qs, many=True)
return Response(serializer.data, status.HTTP_200_OK)
ページネーション
ページネーションは通常API全体で同じ設定を使用しますが、個別に設定することもできます。
全体に設定する場合はconfig/settings.py
に以下を設定します。
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100
}
個々に設定する場合は以下のようにpagination_class
に設定します。
from rest_framework.pagination import LimitOffsetPagination
class StandardResultsSetPagination(LimitOffsetPagination):
default_limit = 2
max_limit = 100
class BillingRecordsView(generics.ListAPIView):
queryset = Billing.objects.all()
serializer_class = BillingRecordsSerializer
pagination_class = StandardResultsSetPagination
上記の方法はListAPIView
などの汎用Viewを使えば適用されるのですが、
APIViewには適用されませんでした。その場合は、以下のようにするようです。
こちらを参考にしました。
from rest_framework.pagination import LimitOffsetPagination
class PaginationHandlerMixin(object):
@property
def paginator(self):
if not hasattr(self, '_paginator'):
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
else:
pass
return self._paginator
def paginate_queryset(self, queryset):
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data):
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
class StandardResultsSetPagination(LimitOffsetPagination):
default_limit = 2
max_limit = 100
# Create your views here.
class TaskCreateListAPIView(views.APIView, PaginationHandlerMixin):
pagination_class = StandardResultsSetPagination
serializer_class = TaskSerializer
""" Taskモデルの登録API """
def post(self, request, *args, **kwargs):
# Serializerを作成
serializer = self.serializer_class(data=request.data)
# バリデーション実行
serializer.is_valid(raise_exception=True)
# モデルオブジェクトを登録
serializer.save()
# JSON文字列をレスポンスとして返す
return Response(serializer.data, status.HTTP_201_CREATED)
def get(self, request, *args, **kwargs):
""" Taskモデルの一覧取得API """
tasks = Task.objects.all()
page = self.paginate_queryset(tasks)
if page is not None:
serializer = self.get_paginated_response(self.serializer_class(page, many=True).data)
else:
serializer = self.serializer_class(tasks, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
PDFをダウンロードするとかカスタムアクションの実装
こちらをそのままコピペ。
import os
from urllib.parse import quote
from django.conf import settings
from django.http import HttpResponse
from rest_framework.views import APIView
class DownloadTermPDF(APIView):
def get(self, request, *args, **kwargs):
filename = 'サービス約款.pdf'
filepath = os.path.join(settings.BASE_DIR, 'static/pdf/term.pdf')
return _pdf_download_response(filename, filepath)
class DownloadAgreementPDF(APIView):
def get(self, request, *args, **kwargs):
filename = 'サービス同意書.pdf'
filepath = os.path.join(settings.BASE_DIR, 'static/pdf/agreement.pdf')
return _pdf_download_response(filename, filepath)
def _pdf_download_response(filename, filepath):
"""
pdfのダウンロードをおこなう
Args:
filepath: ファイルのパス
filename: ファイル名
Returns:
HttpResponse: HttpResponse
"""
with open(filepath, 'rb') as pdf:
response = HttpResponse(content=pdf)
response['Content-Type'] = 'application/pdf'
response['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(filename.encode('utf-8'))}"
return response
APIドキュメント
config/settings.py
に以下を設定します。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party apps
'rest_framework',
'django_filters',
'drf_spectacular',
# my apps
'apiv1.apps.Apiv1Config',
'todo.apps.TodoConfig',
'shop.apps.ShopConfig',
]
...
# REST_FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', # 追加
}
config/urls.py
に以下を記載します。
DEBUG環境時のみドキュメンテーションを見れるようにしています。
URLにアクセスすると綺麗なドキュメントが見れます。
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('apiv1.urls')),
]
if settings.DEBUG:
urlpatterns += [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'), # 追加
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), # 追加
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # 追加
]
上の設定でも最小限は見れるのですが、今回のようなAPIViewを継承してViewを作成している場合細かい設定をするとより情報を見れます。
詳しくはこちらを参考。
from drf_spectacular.utils import extend_schema
class TaskCreateListAPIView(views.APIView, PaginationHandlerMixin):
pagination_class = StandardResultsSetPagination
serializer_class = TaskSerializer
@extend_schema(
request=TaskSerializer,
responses={201: TaskSerializer},
)
def post(self, request, *args, **kwargs):
""" Taskモデルの登録API """
# Serializerを作成
serializer = self.serializer_class(data=request.data)
# バリデーション実行
serializer.is_valid(raise_exception=True)
# モデルオブジェクトを登録
serializer.save()
# JSON文字列をレスポンスとして返す
return Response(serializer.data, status.HTTP_201_CREATED)
@extend_schema(
responses={200: TaskSerializer},
)
def get(self, request, *args, **kwargs):
""" Taskモデルの一覧取得API """
tasks = Task.objects.all()
page = self.paginate_queryset(tasks)
if page is not None:
serializer = self.get_paginated_response(self.serializer_class(page, many=True).data)
else:
serializer = self.serializer_class(tasks, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Discussion