Django rest framework で、Serializer をネストした APIを作成する


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)
    


参考


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")
    

以上です。

コメント