DRFを何度か使ったので, ざっくりまとめておく.
Contents
Django自体の機構には触れない.
環境
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.4
BuildVersion: 19E287
$ python -V
Python 3.7.2
$ pip -V
pip 20.0.2 from /Users/kaito/.pyenv/versions/3.7.2/lib/python3.7/site-packages/pip (python 3.7)
$ pip list | grep pipenv
pipenv 2018.11.26
パッケージ管理には, pipenv
, 静的解析ツールとして mypy
を使うようにする.
導入
# Mypy
$ pipenv install --dev mypy
# Django
$ pipenv install django
$ pipenv install --dev django-stubs ipython django-extensions
# Django Rest Framework
$ pipenv install djangorestframework
$ pipenv install --dev djangorestframework-stubs
初期設定 config/settings.py
を上書き.
# config/settings.py
INSTALLED_APPS = [
...
'rest_framework',
'sample.apps.SampleConfig', # 今回使うアプリ
]
mypy
用設定ファイルの設置.
; mypy.ini
[mypy]
python_version = 3.7
check_untyped_defs = True
disallow_untyped_defs = True
disallow_any_generics = True
disallow_untyped_calls = False
disallow_untyped_decorators = False
ignore_errors = False
ignore_missing_imports = False
strict_optional = True
no_implicit_optional = True
implicit_reexport = False
strict_equality = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
warn_unreachable = True
warn_no_return = True
; Django support
plugins =
mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = config.settings
[mypy-django.core.asgi]
ignore_missing_imports = True
[mypy-*.migrations.*]
ignore_errors = True
[mypy-*.tests.*]
ignore_errors = True
シリアライザ
REST APIにおいては, Client <=> Server 間で, JSON形式でデータがやりとりされる.
言い換えれば, クライアントサイドで扱うJSON形式のデータとPythonオブジェクトとしてのインスタンスが双方向に変換できる必要がある, ということ.
具体的には, 例えば
# sample/models.py
from django.db import models
from django.core.validators import MinLengthValidator
class SampleModel(models.Model):
name = models.CharField(
max_length=20,
validators=[MinLengthValidator(1)],
null=False
)
def __repr__(self) -> str:
return "SampleModel({})".format(self.name)
__str__ = __repr__
このようなモデルが定義されているとき,
instance = SampleModel.objects.create(name="tarou")
{
"pk": 1,
"name": "tarou"
}
これらが等しいレコードを表していて,
GET
メソッドにおいて instance
=> JSON
,
POST
, PUT
, PATCH
, DELETE
メソッドにおいて, JSON
から インスタンスの操作ができる必要があるということだ.
これを担っているのがシリアライザの概念で, 一般的にseriazers.py
に記述していく.
シリアライザの例
基本的には汎用的なシリアライザを使うが, まずはベーシックなものから.
# sample/serialzers.py
from rest_framework import serializers
from .models import SampleModel
class SampleSerializer(serializers.Serializer):
name = serializers.CharField(
max_length=20,
min_length=1,
allow_blank=False,
trim_whitespace=True
)
def create(self, validated_data: Dict[str, str]) -> SampleModel:
created = SampleModel.objects.create(**validated_data)
if isinstance(created, SampleModel):
return created
else:
# ここには到達しない
# 型付けのため
raise RuntimeError
def update(self, instance: SampleModel, validated_data: Dict[str, str]):
for key in validated_data.keys():
instance.__setattr__(key, validated_data.get('name'))
instance.save()
return instance
def to_representation(self, instance) -> Dict[str, str]:
return super().to_representation(instance)
- create: dict から新規オブジェクトの作成
- update: dict から既存オブジェクトのパラメータ更新
- to_representation: インスタンス => dict への変換
を担っている.
シリアライズ(instance => dict)
シリアライザ.data
を参照する
import json
from sample.serialzers import SampleSerializer
from sample.models import SampleModel
tarou = SampleModel.objects.create(name='tarou')
SampleSerializer(instance=tarou).data # {'name': 'jiro'}
# 内部的には,
SampleSerializer().to_representation(instance=tarou) # OrderedDict([('name', 'jiro')])
dataへの変換は, to_representation
が担っているのでカスタマイズしたい場合はここをいじる, 実際に外から使うときは .data
を参照する.
これで, インスタンスからDictへの変換ができた.
デシリアライズ1 (dict => instance 新規作成)
import json
from sample.serialzers import SampleSerializer
json_data = '{"name": "jiro"}'
dict_data = json.loads(json_data)
serializer = SampleSerializer(data=dict_data)
serializer.is_valid() # True
serializer.validated_data # OrderedDict([('name', 'jiro')])
serializer.save() # SampleModel(jiro)
jsonから読みこんだディクショナリを
- シリアライザのコンストラクタに渡す
- is_valid() でバリデーション
- True なら save() でオブジェクト作成
- False ならエラー処理を.
デシリアライズ2 (dict => instance パラメータ更新)
import json
from sample.serialzers import SampleSerializer
tarou = SampleModel.objects.get(name='tarou')
serializer = SampleSerializer(instance=tarou, data={"name": "updated_tarou"})
serializer.is_valid() # True
serializer.save() # SampleModel(updated_tarou)
コンストラクタに, インスタンスとdata両方を渡しているときは save() において update() が呼ばれる.
まとめると,
用途 | コンストラクタ引数 | 補足 |
---|---|---|
シリアライズ | インスタンス | .data にDictが入る |
新規作成 | ディクショナリ | .save(中でcreateが呼ばれる) で新規作成 |
パラメータ更新 | インスタンス & ディクショナリ | .save(中でupdateが呼ばれる) で更新 |
こんな感じ.
partial_update したいときには, コンストラクタに partial=True
を渡す.
参考: Serializers #Partial updates - Django REST framework
ModelSerializer
ほとんどのコードは共通なので抽象化された ModelSerializer を継承して差分を書くほうが効率的だ.
# sample/serializers.py
from rest_framework import serializers
from .models import SampleModel
class SampleModelSerializer(serializers.ModelSerializer):
class Meta:
model = SampleModel
fields = ('pk', 'name',)
extra_kwargs = {
'name': {} # Dictで渡す
}
extra_kwargs にオプションを指定することで細かなカスタマイズができる.
実際に使えるオプションは以下参照.
Serializer fields #Core arguments - Django REST framework
ビューセット
DRFにおいて, view は viewset で定義する
django-filter の導入
ビューでクエリによるフィルタリング(?name=tarouとか)を可能にするパッケージを追加しておく
$ pipenv install django-filter
# config/settings.py
INSTALLED_APPS = [
...
'django_filters',
...
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend'
],
}
これでフィルタリングのデフォルトを指定したので, 各ビューで明示しなくてもクエリによるフィルタリングを使うことができるようになった.
ModelViewSet
from rest_framework import viewsets
from .models import SampleModel
from .serializers import SampleModelSerializer
class SampleModelViewSet(viewsets.ModelViewSet):
queryset = SampleModel.objects.all()
serializer_class = SampleModelSerializer
filter_fields = ('name', )
ModelViewSet に, queryset と serializer_class を指定することで, CRUDを扱うビューが自動生成される.
カスタマイズについては,
アクション | メソッド |
---|---|
動的な query_set の指定 | get_queryset(self) -> QuerySet |
動的な serializer の指定 | get_serializer_class(self) -> Type[serializers.BaseSerializer] |
GET 一覧 | list(self, request, *args, **kwargs) -> Response |
GET 詳細 | retrieve(self, request, *args, **kwargs) -> Response |
POST 新規作成 | create(self, request: Request, *args: Any, **kwargs: Any) -> Response |
PUT 更新 | update(self, request, *args, **kwargs) -> Response |
PATCH 部分更新 | partial_update(self, request, *args, **kwargs) -> Response |
DELETED 削除 | destroy(self, request, *args, **kwargs) -> Response |
この辺りを書き換えることで, 細かな調整が可能だ.
他にも, CRUD操作のうちRead Onlyなビューを自動生成する Viewsets #ReadOnlyViewSet - Django REST framework もある.
ルーティング
アプリケーション下の urls.py
に下のように記述する
# sample/urls.py
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register('sample', views.SampleModelViewSet)
urlpatterns = []
urlpatterns += router.urls
あとは, project の urls.py
にて,
# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('sample.urls')),
]
のようにすれば良い.
トークン認証の追加
トークン認証については, 以前記事を書いたのでそちらのリンクを貼っておく.
APIドキュメントの自動生成
DRFには, シリアライザ定義を参照することでAPIドキュメントを自動生成できる axnsan12/drf-yasg - github がある.
drf-yasg の導入
$ pipenv install drf-yasg
$ pipenv install --dev drf-yasg-stubs
セットアップ
# config/settings.py
INSTALLED_APPS = [
...
'drf_yasg',
...
]
# sample/urls.py
from django.urls import path
from rest_framework import routers
from rest_framework.permissions import AllowAny
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from . import views
app_name = 'sample'
router = routers.DefaultRouter()
# Endpoint Config
router.register('sample', views.SampleModelViewSet)
urlpatterns = []
urlpatterns += router.urls
# Documentation
schema_view = get_schema_view(
openapi.Info(
title="Sample API",
default_version='v1',
description="Sample Api",
terms_of_service="https://example.com",
contact=openapi.Contact(email="mail.kaito03@gmail.com"),
license=openapi.License(name="MIT License"),
),
public=True,
# ⇓ 型定義では, Tuple[str] 求めてるのに, 実行してみるとパーミッションクラスを必要としてる ⇓
# ⇓ とりあえず, 無視しておく ⇓
permission_classes=(AllowAny,), # type: ignore
)
urlpatterns += [
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]
; mypy.ini
plugins =
mypy_django_plugin.main, mypy_drf_plugin.main
...
; Ignore Package Imports
[mypy-ruamel.*]
ignore_missing_imports = True
これで,
にAPIドキュメントが自動生成されるようになった.
シリアライザをきちんと定義しておけば, ドキュメントも適切に定義される.
@action
デコレータでエンドポイントを追加した場合は,
from rest_framework import viewsets, status, serializers
from rest_framework.decorators import action
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
class SampleModelViewSet(viewsets.ModelViewSet):
queryset = SampleModel.objects.all()
serializer_class = SampleModelSerializer
filter_fields = ('name', )
@action(['GET'], detail=False, permission_classes=(permissions.IsAuthenticated,))
@swagger_auto_schema(
request_body=SampleModelSerializer,
responses={
200: openapi.Response('成功.', SampleModelSerializer),
401: openapi.Response('Bad Request.')
}
)
def additional_endpoint(self, request: Request) -> Response:
return Response(data={}, status=status.HTTP_200_OK)
このように, swagger_auto_schema デコレータを介して Request Body や レスポンスに対してシリアライザを指定できる.