Blog の関連取得用の、API を作成したのですが、Django rest framework の実装で少し手間取りましたので、作成した APIと手間取ったところについて記載します。
前提
以下の環境で動作検証は実施しています。
-
OS
% sw_vers ProductName: Mac OS X ProductVersion: 10.13.3 BuildVersion: 17D47
-
Python の version
% python3 -V Python 3.6.2
-
Django の version
python3 -m pip list | grep Django Django (1.11.11)
-
Djagno rest framework の version
python3 -m pip list | grep rest djangorestframework (3.6.4)
参考
- Serializer relations - Django REST framework
- Filtering - Django REST framework
- python - Django Rest Framework object is not iterable? - Stack Overflow
API で取得する Model のリレーションについて
リレーションは以下の通りです。PlantUML のer図で記載しました。
Blog Entry と、関連記事テーブルがあり、ある Blog 記事の関連記事の情報を、取得したいです。
Model の実体は、puput/models.py at master · APSL/puput の、EntryPageRelated
、および、EntryAbstract
になります。
作成したAPI
以下、APIを作成しました。
rest_framework.py
from puput.models import EntryPage
from puput.models import EntryPageRelated
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework import serializers, viewsets
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from django.http import Http404
class EntryPageSerializer(serializers.ModelSerializer):
class Meta:
model = EntryPage
fields = ('id','title', 'slug', 'gist_id')
# Serializers define the API representation.
class EntryPageRelatedSerializer(serializers.ModelSerializer):
entrypage_from = serializers.SlugRelatedField(
many=False,
read_only=True,
slug_field='gist_id')
entrypage_to = EntryPageSerializer(many=False, read_only=True)
class Meta:
model = EntryPageRelated
fields = ('entrypage_from','entrypage_to')
# ViewSets
class EntryPageRelatedViewSet(ReadOnlyModelViewSet):
serializer_class = EntryPageRelatedSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
gist_id = self.kwargs['pk']
queryset = EntryPageRelated.objects.all()
if gist_id:
entry_page = EntryPage.objects.filter(gist_id=gist_id).first()
queryset = queryset.filter(entrypage_from=entry_page)
return queryset
"""
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
queryset = self.get_queryset()
if not queryset:
raise Http404("Not Found..")
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
説明
プログラムについて説明します。
-
EntryPageSerializer
これが Nest した Serializer になります。 -
EntryPageRelatedSerializer
これは、API で使用する Serializer です。
内部で、フィールド、entrypage_to は、EntryPageSerializer
を参照しています。
フィールド、entrypage_from は、特に Model のフィールドを必要としなかったため、SlugRelatedField
で gist_id のみを取得するようにしました。 -
EntryPageRelatedViewSet
API となる ViewSet です。-
get_queryset URLの一部として渡される gist_id で結果を絞り込みたかったので、
get_queryset
内で、パラメータ pk を取得し、その結果の有無で全件取得するか絞り込みするかを切り替えています。 -
retrieve
pk での絞り込みしたデータ取得のための、retrieve
メソッドをオーバーライドしました。
おそらく正しい使い方ではなく、本来の実装で、get_queryset
の結果が、予期せず更に絞り込まれてしまい、結果が 0 件になっていたので、止むを得ずオーバーライドしました。おそらくもっといいやり方があるのかと思います。
実はこのケースは、ReadOnlyModelViewSet
を使用すべきではないのかもしれません。
-
OutPut
APIを実行すると以下の結果が返されます。
これは期待通りです。
[
{
"entrypage_from": "3459f9cd97ac22b201336931759dc4d9",
"entrypage_to": {
"id": 67,
"title": "初台で花見をした",
"slug": "acbc4d1aa0a28f781d180b87a8e8fb3c",
"gist_id": "acbc4d1aa0a28f781d180b87a8e8fb3c"
}
},
{
"entrypage_from": "3459f9cd97ac22b201336931759dc4d9",
"entrypage_to": {
"id": 45,
"title": "all.json を一次加工後に、手作業で変換したRedPen の辞書ファイル",
"slug": "3df15ae935eb394972f9bdd2f87d43a2",
"gist_id": "3df15ae935eb394972f9bdd2f87d43a2"
}
},
{
"entrypage_from": "3459f9cd97ac22b201336931759dc4d9",
"entrypage_to": {
"id": 63,
"title": "GIthub Repository フォーク後、PullRequest作成までに実行するコマンドのメモ書き",
"slug": "e6a04e124e1771e3e92f863ebbd27229",
"gist_id": "e6a04e124e1771e3e92f863ebbd27229"
}
},
{
"entrypage_from": "3459f9cd97ac22b201336931759dc4d9",
"entrypage_to": {
"id": 46,
"title": "all.json から RedPen の SuggestExpression ルールの辞書ファイルを作成する",
"slug": "4faae2b829480531826ff6bfa4745d9e",
"gist_id": "4faae2b829480531826ff6bfa4745d9e"
}
},
{
"entrypage_from": "3459f9cd97ac22b201336931759dc4d9",
"entrypage_to": {
"id": 41,
"title": "scikit-surprise の 入力ファイル向けに、spreadsheet の データを変換するスクリプト",
"slug": "47dcc7f47262a5972286b1b0fd624910",
"gist_id": "47dcc7f47262a5972286b1b0fd624910"
}
}
]
発生したトラブルについて
Serializer に対する many 属性の指定
-
エラー内容
'Model' object is not iterable
-
対処
Nest した Serializer で、object is not iterable
エラーが発生していました。
Serializer には、many 属性を指定できますが、この指定をしていなかったため、エラーが発生していました。
私のケースでは、False
の指定が必要で、デフォルトだと、True
で動作するのかと思います。
entrypage_to = EntryPageSerializer(many=False, read_only=True)
pkキーワードが含まれていない
-
エラー内容
AssertionError: Expected view EntryPageRelatedViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
-
対処
<pk>
キーワードを追加した。
url(r'^related/(?P<pk>.+)/$', entry_page_related, name='entry_page_related'),
-
補足
<pk>
キーワードは、ViewSet のlookup_field
の設定で変更することができます。
lookup_field = 'my_pk'
ViewSet に querysetフィールドが定義されていない
-
エラー内容
assert queryset is not None, '`base_name` argument not specified, and could ' \ AssertionError: `base_name` argument not specified, and could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute.
-
対処
Router で url を定義する際、base_name の指定がない場合は、queryset を元に basename を決定する動作になるようです。
ViewSet には、get_queryset ファンクションを定義していて、queryset を指定していなかったので、router.register で base_name を指定するようにしました。
# ex) router.register(r'snippets', views.SnippetViewSet, "snippests")
以上です。
コメント