[DRF]apiの作成(patch)【partial=True】


Django-rest-framework勉強中です

前回までに書いた記事はこちらのとおりです。

記事
[DRF]apiの作成(post)
[DRF]apiの作成(get)part2
[DRF]apiの作成(get)part1

準備

前回までと同じですが、一応Model構成載せておきます。

models.py
from django.db import models

DISTRICT_CATEGORIES = [
    (1, "地区1"),
    (2, "地区2"),
    (3, "地区3"),
    (4, "地区4"),
]


class Student(models.Model):
    """生徒情報"""

    # 生徒ID
    student_id = models.CharField(max_length=4, primary_key=True)

    # クラス
    class_no = models.CharField(max_length=1)

    # 出席番号
    attendance_no = models.IntegerField()

    # 名前
    name = models.CharField(max_length=20)

    # 地区番号
    district_no = models.CharField(max_length=1, choices=DISTRICT_CATEGORIES)

    # フリーコメント
    comment = models.CharField(max_length=200,blank=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["class_no", "attendance_no"],
                name="class_attendance_unique"
            ),
        ]
STUDENT ID CLASS NO ATTENDANCE NO NAME DISTRICT NO COMMENT
0005 2 2 fugafuga 地区1 2組2番
0004 2 1 hogehoge 地区2 2組1番
0003 1 3 山田3番 地区4 1組3番
0002 1 2 山田花子 地区2 1組2番
0001 1 1 山田太郎 地区1 1組1番

リソースの一部更新

特定のカラムのupdateのapiを作りたいです。restではupdateはpatchとputがあります。putはどちらかというと"置換"を表すようなので、今回はpatchが正しそうですね。patchメソッドを用いたapiを作成していきましょう。

serializer

地区番号だけもらって、更新をする引っ越しAPIとしましょう。

serializer.py
from rest_framework import serializers
from ..models import Student


class StudentSerializer(serializers.ModelSerializer):
    """Studentクラスのシリアライザ"""

    class Meta:
        model = Student
        fields = [
            'district_no'
        ]

view

とりあえずAPIViewからいきますよ。
is_valid()とかやってるところは同じなのですが、シリアライザの作成の仕方が今までと違いますね。今まではシリアライザにはrequest.dataを引数として渡していましたが、違うものがくっついています。
ここです。
serializer = StudentSerializer(instance=instance, data=request.data, partial=True)

第一引数の instanceなのですが、ここをつけておくと後続のsave()の挙動が変わります。
第一引数がない→create
第一引数がある→update
です。

さらに、partial=Trueをつけておくとrequest.dataの値のみで更新をしてくれるみたいです。(request.dataの値は一部のみで良い)

views.py
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response

from ..models import Student
from ..serializers import StudentSerializer


class StudentDistrictUpdateAPIView(APIView):

    def patch(self, request, pk, *args, **kwargs):
        """
        パスパラメータのpkを受け取りbodyの地区番号に更新
        """
        # instanceの取得
        instance = get_object_or_404(Student, pk=pk)

        # シリアライザの作成
        serializer = StudentSerializer(instance=instance, data=request.data, partial=True)

        # バリデーション
        serializer.is_valid(raise_exception=True)

        # DB更新
        serializer.save()

        return Response({'result':True})

実行

URL, 実行ファイルについては割愛します。

student_id='0001'のデータをdistrict_no='2'で更新します。

更新前

STUDENT ID CLASS NO ATTENDANCE NO NAME DISTRICT NO COMMENT
0001 1 1 山田太郎 地区1 1組1番

更新後

STUDENT ID CLASS NO ATTENDANCE NO NAME DISTRICT NO COMMENT
0001 1 1 山田太郎 地区2 1組1番

ちゃんと更新してくれましたね!

partial=Trueについて

ちょっと難しかったというか、イメージがつかなかったのでいろいろ試して説明してみます。

まずは流れるSQL。

partial=TrueがついてるバージョンのSQL

UPDATE "students_student" SET "class_no" = '1', "attendance_no" = 1, "name" = '山田太郎', "district_no" = '2', "comment" =1番' WHERE "students_student"."student_id" = '0001'; args=('1', 1, '山田太郎', '2', '1組1番', '0001')

partial=TrueがついてないバージョンのSQL

UPDATE "students_student" SET "class_no" = '1', "attendance_no" = 1, "name" = '山田太郎', "district_no" = '2', "comment" =1番' WHERE "students_student"."student_id" = '0001'; args=('1', 1, '山田太郎', '2', '1組1番', '0001')

同じですね・・・てっきりrequest.dataのところだけ更新するSQLが流れると思っていましたが、違いました。

serializer

ここに違いがありました。
今まではserializerは以下の定義だったので問題なしです。

    class Meta:
        model = Student
        fields = [
            'district_no'
        ]

ですが、serializerをこのようにfields全体で指定した場合のことを考えます。

    class Meta:
        model = Student
        fields = '__all__'

この場合、partial=Trueをつけないと、validationで怒られます。

{"student_id":["この項目は必須です。"],"class_no":["この項目は必須です。"],"attendance_no":["この項目は必須です。"],"name":["この項目は必須です。"]}

必須な項目がないよってことですね。patchリクエストでどの項目をupdateするかをAPIリクエスト時に決めるようなAPIであればfieldsはallにして、リクエスト次第で決めるという作りにしたいときがあると思います。
こういうときにpartial=Trueを使うようです。
partial=Trueをつけるとうまく動きました。

{"result":true}

汎用APIViewを使用

汎用APIViewを使用してpatch処理をしてみます。

view

以下のようになりました。

views.py
from rest_framework import generics

from ..models import Student
from ..serializers import StudentSerializer


class StudentDistrictUpdateAPIView(generics.UpdateAPIView):
    queryset = Student.objects.all()
    serializer_class = StudentSerializer

おいおいまじか・・・ってくらいのコード量ですが。。以下が実行結果です。地区番号を4に更新しています。

{"student_id":"0001","class_no":"1","attendance_no":1,"name":"山田太郎","district_no":"4","comment":"にゃ〜"}

汎用APIViewmだとデフォルトではupdate後のinstanceをそのまま返してくれるみたいですね。いやー、ほんとにコード量少なくてすごい。なにしてるのか分かりづらい。。。

一旦基礎編は終了です

今までget, post, patchをやってきて、主要なAPIは作成できたかなと思います。
次回からは応用編ということで、外部キー接続しているテーブルを操作するAPIについて作成していこうと思います。