DjangoのRESTframeworkを使っている際に調べた内容で個人的にメモとして残しておきたい項目をまとめました。
調べたら当たり前の内容や自分で実装した項目まであります。本記事の内容でもっと良い方法があったり、そもそもこんな問題に直面することが間違っているなどの指摘がありましたら、コメント等でお願いします。
ネストしたURLを表現したい
restframeworkのrouterを使って以下のようなrest apiを作成するのは容易です。
/api/v1/groups/ GET POST
/api/v1/groups/1/ GET PUT PATCH DELETE
/api/v1/members/ GET POST
/api/v1/members/1/ GET PUT PATCH DELETE
しかし以下のようにurlがネストしたapiをrestframeworkのrouterで作成するのは困難です。
/api/v1/groups/ GET POST
/api/v1/groups/1/ GET PUT PATCH DELETE
/api/v1/groups/1/members/ GET POST
/api/v1/groups/1/members/1/ GET PUT PATCH DELETE
解決方法
解決方法はdrf-nested-routersを使います。
drf-nested-routersはネストしたurlをrestframework上で簡単に実現できるライブラリです。
導入方法
pipインストールを行います。
$ pip install drf-nested-routers
実装
- 一階層目のrouterはrestframewrok標準のものではなく、drf-nested-routers専用のrouter(SimpleRouter)を使用します。
- 一階層目のViewをregister()メソッドで登録します。
- 二階層目のrouterを用意します。二階層目ではNestedSimpleRouterを使用して一階層目と二階層目のrouterを紐付けます。
- NestedSimpleRouterインスタンス(groups_router)のregister()メソッドで二階層目のViewを登録します。
- 最後にurlpatternsの中に一階層目のrouter.urls
# urls.py
from rest_framework_nested import routers
from .views import *
router = routers.SimpleRouter()
router.register(r'groups', GroupViewSet)
groups_router = routers.NestedSimpleRouter(router, r'groups', lookup='group')
groups_router.register(r'members', MemberViewSet, base_name='groups-members')
urlpatterns = [
url(r'^api/v1/', include(router.urls)),
url(r'^api/v1/', include(groups_router.urls)),
]
以下のように引数でそれぞれのprimary_keyを取得できます。引数のキーワード名はurls.pyで指定したlookup名+_pkです。
# views.py
class GroupViewSet(viewsets.ViewSet):
def list(self, request):
(...)
return Response(serializer.data)
def retrieve(self, request, pk=None):
group = self.queryset.get(pk=pk)
(...)
return Response(serializer.data)
class MemberViewSet(viewsets.ViewSet):
def list(self, request, group_pk=None):
members = self.queryset.filter(group=group_pk)
(...)
return Response(serializer.data)
def retrieve(self, request, pk=None, group_pk=None):
member = self.queryset.get(pk=pk, group=group_pk)
(...)
return Response(serializer.data)
ModelViewSetで実装されたREST APIのPOSTで一度に複数のモデルを作成したい
実は標準のviews.ModelViewSetのcreate()メソッドでは一度に複数のモデルを作成することができません。複数のモデルを作成したかったらその分だけAPIを叩かなければなりません。
解決方法
作成したコード
そこで、views.ModelViewSetで単体および複数のモデルを作成できるデコレーターを作成しました。
以下のコードをコピーして適当なファイルに保存します。
from rest_framework.response import Response
from rest_framework import status
def multi_create(serializer_class=None):
def __multi_create(function):
def __wrapper(self, request, *args, **kwargs):
many = False
if isinstance(request.data, list):
many = True
serializer = serializer_class(data=request.data, many=many)
if serializer.is_valid():
serializer.save()
headers = self.get_success_headers(serializer.data)
data = serializer.data
result = function(self, request, *args, **kwargs)
if result is not None:
return result
if many:
data = list(data)
return Response(data,
status=status.HTTP_201_CREATED,
headers=headers)
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
return __wrapper
return __multi_create
使い方
以下のようにmulti_createデコレーターを先ほど保存したファイルからimportしてViewSetのcreate()メソッドにつけます。引数は作成したいモデルに対応したSerializerのクラスです。
# views.py
from .decorators import multi_create
class MyViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MySerializer
@multi_create(serializer_class=MySerializer)
def create(self, request):
pass
あとは以下のようなリスト形式のJSONデータをPOSTするだけです。
[
{"name": "hoge"},
{"name": "fuga"}
]
以下のようなレスポンスが返ってきます。
[
{
"id": 1,
"name": "hoge"
},
{
"id": 2,
"name": "fuga"
}
]
Serializerのフィールド値を動的に決めたい
Serializerのフィールド値を動的に決めたい場合があります。
解決
今回はserializers.SerializerMethodField()を使います。
serializers.SerializerMethodField()を使うことにより、メソッドの結果によってフィールドの値を決めることができます。
以下のようなModelクラスとname + _hogeを返すhoge()メソッドがあったとします。
# modles.py
class MyModel(models.Model):
name = models.CharField(max_length=100)
def hoge(self):
return "{}_hoge".format(self.name)
Serializerでは以下のようにserializers.SerializerMethodField()を指定することのよりvalueフィールドの値を動的に決定します。適用されるメソッド名はget_ + フィールド名です。今回はget_value()メソッドの返却値がvalueの値となります。
また適用されるメソッド名をSerializerMethodField()のmethod_nameという引数で指定することも可能です。
# serializer.py
class MySerializer(serializers.ModelSerializer):
value = serializers.SerializerMethodField()
class Meta:
model = MyModel
def get_value(self, obj):
return obj.hoge()
ModelやSelializerなどで発生したエラーをレスポンスとして返したい
APIが叩かれViewSetのcreate()メソッドが呼ばれたとします。その際に以下のようなModelクラスのsave()メソッドでエラーが発生した場合はどうやってエラーレスポンスを行えばいいのでしょうか。
try exceptでエラーハンドリングをするにもMyViewSetクラスには自分で実装したメソッドはなく、MyModelのsave()メソッドは完全にブラックボックスの中で呼ばれています。
# views.py
class MyViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MySerializer
# models.py
class MyModel(models.Model):
name = models.CharField(max_length=100)
def save(self, force_insert=False, force_update=False,
using=None, update_fields=None):
if self.hoge():
raise HogeError('hoge error.')
super(MyModel, self).save(*args, **kwargs)
def hoge():
(...)
解決方法
1つ目
一つの解決方法としては以下のようにcreate()メソッドをオーバライドしてエラーハンドリングを行うことです。
# views.py
class MyViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MySerializer
def create(self, request):
try:
super(MyViewSet, self).create(*args, **kwargs)
except HogeError:
(....)
return Response(content, status=status.HTTP_400_BAD_REQUEST)
def update(self, request):
try:
super(MyViewSet, self).update(*args, **kwargs)
except HogeError:
(....)
return Response(content, status=status.HTTP_400_BAD_REQUEST)
この方法ではcreateとupdateの際にそれぞれ同じようにErrorのハンドリングを行う必要が出てきます。
2つ目
そこでもう一つの解決方法としてhandle_exception()メソッドをオーバライドする方法です。
handle_exceptionはrestframeworkの標準でエラーハンドリングを行ってくれるメソッドです。
たとえば、許可されていないHTTPメソッドを叩くと以下のようなレスポンスを返します。
HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 42
{"detail": "Method 'DELETE' not allowed."}
この方法ではhandler_exceptionでexceptされていないエラーをオーバーライド先でexceptしてしまうという方法です。
# views.py
class MyViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MySerializer
def handle_exception(self, exc):
try:
return super(MyViewSet, self).handle_exception(exc)
except HogeError:
content = {'detail': '{}'.format(exc.args)}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
この方法を使うことによってこのMyViewSet内で発生したエラーを全てハンドリングすることができます。
ちなみにtry exceptではなく、excの型をisinstanceで判定する方法でも問題はありません。
3つ目
3つ目がcustom_exception_handler()を使う方法です。
settings.pyにこれから実装するcustom_exception_handlerのpathを記述します。
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
先ほどのpathで指定したファイルにcustom_exception_handler()を実装します。
# utils.py
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if isinstance(exc, HogeError):
content = {'detail': '{}'.format(exc.args)}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
return response
この方法の特徴としてはすべてのViewで発生したエラーがこのcustom_exception_handlerに集約されます。
これらの方法はそれぞれスコープが異なるので状況によって使い分けたいところです。
Viewの値をSerializerに渡したい
解決方法
解決方法は考えてみれば当たり前の話でSerializerのコンストラクタ(init)に渡してあげれば良いのです。
今回の例ではuser_dataというキーワード引数に渡しています。
# views.py
class MyViewSet(views.ModelViewSet):
def retrieve(self, request):
user_data = request.GET['user_data']
(...)
serializer = MySerializer(My_list, many=True, user_data=user_data)
受け取る側では__init__をオーバライドしてキーワード引数から受け取ります。
# serializer.py
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
self.user_data = kwargs.pop('user_data', '')
super(MySerializer, self).__init__(*args, **kwargs)
Viewの値をSerializerに渡すことはあまりないと思いますが、serializers.SerializersMethodFiels()などを使用する際は使うこともあるかもしれません。
以上です。
参考
- drf-nested-routers
https://github.com/alanjds/drf-nested-routers - Serializer fields - Django REST framework
http://www.django-rest-framework.org/api-guide/fields/#serializermethodfield - Custom throttling response in django rest framework - CodeDump
https://codedump.io/share/vqgOBceUoFXC/1/custom-throttling-response-in-django-rest-framework - Exceptions - Django REST framework
http://www.django-rest-framework.org/api-guide/exceptions/