アプロダ、REST APIの実装②

Django REST framework

概要

DRFとVueで、ファイルアップローダーを作るシリーズの1つです。Django REST frameworkの処理を作りこんでいきます。

紐づくデータを含めるようにする

file1.txt
file2.png
dir1/
    file3.txt
    dir2/
        dir3/
dir4/
    file4.txt

上は、ファイル・ディレクトリを適当に作ってみた例です。今回のアップローダーですが、トップページでは、file1.txt、file2.png、dir1、dir4が表示されます。/dir1/というURLでアクセスするとfile3.txtとdir2、/dir4/ならばfile4.txtが表示されます。まとめると、対象ディレクトリをURLで指定して、中のファイル・ディレクトリ一覧を表示していくのです。

これをどう表現するかは色々なやり方がありそうです。どうするかというと、今のところ、ある単体のデータを取得すると次のようなJSONの結果となりますが...

{
    "pk": 3,
    "name": "dir1",
    "is_dir": true,
    "src": null,
    "parent": null,
    "zip_depth": 0
}

この段階で、次のように紐づく子データも取得してもらうのです。

{
    "pk": 3,
    "name": "dir1",
    "is_dir": true,
    "src": null,
    "parent": null,
    "zip_depth": 0,
    "composite_set": [
        {
            "pk": 4,
            "name": "file3.txt",
            "is_dir": false,
            "src": "http://127.0.0.1:8000/media/file3.txt",
            "parent": 3,
            "zip_depth": 0,
        },
        {
            "pk": 5,
            "name": "dir2",
            "is_dir": true,
            "src": null,
            "parent": 3,
            "zip_depth": 0,
        },
    ]
}

composite_setというリストを作るのが難しそうに見えますが、Djangoの機能で簡単に作ることができます。

さっそく取り掛かりましょう。データをどのように出力するかはシリアライザーの仕事なので、シリアライザーに手をいれます。まずは、nuploader1/serializers.pyを次のようにします。

from rest_framework import serializers
from .models import Composite


class CompositeSerializer(serializers.ModelSerializer):

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'src', 'parent', 'zip_depth', 'composite_set')

fieldsにcomposite_setを足しただけです。

Compositeモデルは再帰的な構造のモデルで、parent = models.ForeignKey(Composite)といった感じのフィールドがありました。この場合、あるCompositeモデルインスタンスに紐づくCompositeの一覧はcomposite.composite_set.all()のようにして取り出せます。通常のDjangoでもよく出てくる処理でした。post.comment_set.all()みたいなコードを何処かで見かけたはずです。

通常のモデルフィールドと同様に、composite_setのような属性もfieldsに指定することができるという訳です。便利ですね。

composite_set

すると、作られるJSONにもcomposite_setというリストが増えました。とはいえ、これはまだまだ改良の余地があります。

読み取り専用にする

画像下部の入力フォーム部分を見ると、composite_setという項目があって、紐づくCompositeを選択できるようになっています。シリアライザーはJSON等への変換を行いますが、送信されてきたデータの変換も行います。つまり、fieldsに指定した各フィールドは表示される項目であると同時に、送信可能だったり、送信を期待する項目...イメージとしては、入力フォームの各入力欄等にもなる訳です。モデルフォームの各フィールドと同様ですね。

場合によっては、表示だけに使いたいフィールドも出てきます。管理者等が裏側で設定するようなフィールドだってあるでしょう。そんなときは、次のようにして表示専用のフィールドに変更できます。

class CompositeSerializer(serializers.ModelSerializer):

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'src', 'parent', 'zip_depth', 'composite_set')
        read_only_fields = ('composite_set',)

read_only_fieldsですね。こうすると、このシリアライザーを使っての処理でcomposite_setを変更できなくなります。具体的には、CompositeViewSetでは変更できないということですね。もちろん、Django管理サイトでは変更できますし、変更できるシリアライザーを別途作ることもできます。

pkではなく詳細なデータを表示

現状、composite_setはpkのリストです。pk以外のデータ、例えばnameとかis_dirとか、他フィールドも一緒に欲しいかもしれません。こういう場合ですが、まずは簡単に、ブログの記事とコメントのような関係だった場合を例に説明します。Commentモデルは、target=models.ForeignKey(Post)のようにしてPostと紐づいているとします。

class CommentSerializer(serializers.ModelSerializer):

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'target')


class PostSerializer(serializers.ModelSerializer):
    comment_set = CommentSerializer(many=True, read_only=True)

    class Meta:
        model = Composite
        fields = ('pk', 'title', 'text', 'comment_set')

fields = ('pk', 'title', 'text', 'comment_set')の段階で、pk、title、text、comment_setが表示されるようになりますが、comment_setはpkのリストです。そこで、comment_setの表示を上書きするために、クラス属性にcomment_set = CommentSerializer(many=True, read_only=True)として上書きすることができます。この辺も、モデルフォームと同様ですね。

serializers.IntegerFieldserializers.CharFieldといった数値や文字列の単純なフィールド等もありますが、数値や文字列ではなく、{pk: 1, name: "名無し", target: 1}のようなオブジェクト表現が欲しい場合はシリアライザーを利用します。複数のデータが予想される場合はmany=Trueを、そしてread_only引数はここで指定することもできます。

話を戻して、Compositeモデルの場合を考えましょう。Compositeモデルは再帰的な構造ですが、やることは似ています。次のようにしましょう。

class SimpleCompositeSerializer(serializers.ModelSerializer):

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'zip_depth', 'parent')


class CompositeSerializer(serializers.ModelSerializer):
    composite_set = SimpleCompositeSerializer(many=True, read_only=True)

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'src', 'parent', 'zip_depth', 'composite_set')

シリアライザーは、1モデルにつき1つだけって訳じゃありません。必要な分だけ作れます。

あとですが、SimpleCompositeSerializerのfieldsは、使わなそうなフィールドを省いています。srcは今後作るVue.js側の処理で、特に必要なかったので消しています。composite_setも、含めないように注意してください。含めてしまうと再帰的に表示され、ディレクトリの階層が100ぐらい深い物があれば、それら全てを表示しようとしてしまいます。

これはパフォーマンス的にも問題が出てきますし、今回はあくまで単一のCompositeに紐づくComposite、言い換えるとディレクトリ内直下のファイル・ディレクトリの一覧だけ欲しいので、その中、更に中のものは不要です。

Metaのオプションにはdepth = 1のような指定ができ、これによってもオブジェクトとして表示することもできるのですが、処理をカスタマイズしたい場合には使えません。今回の場合、次の説明するparent詳細表示で特殊なカスタマイズが必要なので、depthオプションは見送りました。

親データも詳細表示

CompositeSerializerのparentも、詳しい表示にしておくことにします。先ほどと同様の流れで実装してみましょう。

class CompositeSerializer(serializers.ModelSerializer):
    parent = SimpleCompositeSerializer()
    composite_set = SimpleCompositeSerializer(many=True, read_only=True)

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'src', 'parent', 'zip_depth', 'composite_set')

parentの値は変更できるようにしたいので、read_onlyは不要です。また、単体のデータなので、many=Trueも不要です。

parent

試しにブラウザで確認してみると、表示は問題なさそうです。しかし、ページ下部のフォーム部分を見てください。親ディレクトリを変更したいだけなのに、面倒なことになっています。

実際に送信してみると、The.update()method does not support writable nested fields by default. Write an explicit.update()method for serializernuploader1.serializers.CompositeSerializer, or setread_only=Trueon nested serializer fields.といったエラーも出てしまいます。

やりたいことをまとめると、parentの値を設定・変更したい場合はpkを直接送信し、表示するときはSimpleCompositeSerializerを使ってオブジェクトとして表示したいのです。そこで、次のようにしましょう。

from rest_framework import serializers
from .models import Composite


class SimpleCompositeSerializer(serializers.ModelSerializer):

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'zip_depth', 'parent')


class SimpleCompositeRelation(serializers.RelatedField):

    def to_representation(self, value):
        return SimpleCompositeSerializer(value).data

    def to_internal_value(self, data):
        return self.get_queryset().get(pk=data)


class CompositeSerializer(serializers.ModelSerializer):
    parent = SimpleCompositeRelation(queryset=Composite.objects.filter(is_dir=True), required=False, allow_null=True)
    composite_set = SimpleCompositeSerializer(many=True, read_only=True)

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'src', 'parent', 'zip_depth', 'composite_set')


まず、serializers.RelatedFieldを継承したSimpleCompositeRelationクラスを作りました。これはForeignKeyやOneToOne、ManyToManyなフィールドを表現するのに使えます。parentはForeignKeyでしたね。先ほど説明したように複雑な動作をさせたい場合は、このクラスを使って細かい挙動を制御することができます。

to_representationは、モデルインスタンスをどのように表現するかです。SimpleCompositeSerializer(value).dataとしているように、SimpleCompositeSerializerを使ってオブジェクトとして表現するようにします。{pk: 1, name: "dir2"...} みたいな感じです。

to_internal_valueは、モデルインスタンスへの複合化をどのようにするかの処理です。pkが送信されてくる予定なので、self.get_queryset().get(pk=data)としています。何となくわかると思いますが、これはComposite.objects.all().get(pk=data)のような処理を内部で行います。後程紹介する、queryset引数と関連しています。

ちなみにですが、上書きしなかった以前のparentの値は単純なpkでした。デフォルトではPrimaryKeyRelatedFieldというのが使われていたのですが、これは次のように実装されていたという訳です(簡略化しています)。

class PrimaryKeyRelatedField(RelatedField):

    def to_internal_value(self, data):
        return self.get_queryset().get(pk=data)

    def to_representation(self, value):
        return value.pk

to_representationでオブジェクトのpkを返すようにしていますので、返されるJSONでは単純にpkだけ表示されていました。また、pkの値を直接送信するので、to_internal_valueではpk=dataとしてモデルインスタンスを取得していますね。

このように作ったクラスを、parent = SimpleCompositeRelation(queryset=Composite.objects.filter(is_dir=True), required=False, allow_null=True)として使っています。ここで渡したqueryset引数が、先ほど紹介したget_queryset()で参照されます。なので先ほどのコードは、Composite.objects.filter(is_dir=True).get(pk=data)となる訳です。is_dir=Trueに絞り込んでいるのは、親ディレクトリの指定なのでファイルはダメということです。

required=Falseですが、これは次のような送信データを許可するということです。

{
    name: "dir3",
    is_dir: true,
    zip_depth: 0,
}

上の送信データですが、srcやparentの値を送信していません。もし全てのフィールドがrequired=Trueならば、次のように送信する必要があります。

{
    name: "dir3",
    is_dir: true,
    zip_depth: 0,
    src: null,
    parent: null,
}

また、PUTではなくPATCHメソッドを使った場合も同様で、値を送信しなくても良くなります。極端な話、PATCHでは入力欄がなくて、送信ボタンしかないようなフォームでも多くの場合は動作します。PUTだった場合、今回のモデルではnameフィールドがrequired=Trueになっているので、送信ボタンしかないフォームで送信するとnameを送信しろと怒られます。このPUTとPATCHの使い分けは、PATCHは一部更新に使うもので、PUTは更新というかデータそのものを置き換えるといったニュアンスに近いです。なので必須のフィールドの値を含めていないと、その情報だけじゃ置き換える為の新しいデータが作れないよ!といった感じで怒られるのです。

allow_null=Trueにすると、空の値を送信できます。上の例だと、srcやparentがnullを送信できて、ちゃんと処理されますが、これはallow_null=Trueだからセーフということです。

最後に、少しコードを加えましょう。

class SimpleCompositeRelation(serializers.RelatedField):

    def to_representation(self, value):
        return SimpleCompositeSerializer(value).data

    def to_internal_value(self, data):
        return self.get_queryset().get(pk=data)

    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        # https://github.com/encode/django-rest-framework/issues/5141
        return dict([
            (
                item.pk,
                self.display_value(item)
            )
            for item in queryset
        ])

SimpleCompositeRelationにget_choicesメソッドを追加しました。ブラウザでアクセスした際の選択肢を作成している部分の処理になるのですが、serializers.RelatedFieldのデフォルトの処理では、to_representationは数値や文字列などの単純な値を返すと想定しています。しかし今回はSimpleCompositeSerializer(value).dataを返しています。こういった場合は、今回のような上書きが必要になります。

Relation Posts

Comment

記事にコメントする

まだコメントはありません。