django rest framwork で request.get() が取れない場合. PrimaryKeyRelatedField とか


django/djangorestframework で JOIN する 親から子、子から親
https://qiita.com/uturned0/items/973b32be719a52947f3c の続き

background

GETはrelationしてるobjectを全部、POSTは relationsしているobjectのpkのみを受け取る、というのをしようとして↓を勉強。ありがとう。

やってみると、 update のときに item_id が None になる問題にハマっていた. 理解するためにまず non-relation で勉強

modelはこれ

class Item(models.Model):
    name = models.CharField(max_length=100)
    tel = models.CharField(max_length=100)

    def __str__(self):
        return self.name

serializer

class ItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = Item
        fields = ['id', 'name', 'tel']

    def create(self, request):
        return Item.objects.create(**request)

    def update(self, instance, request):
        item_id = request.pop('item_id', None)

        if item_id:
            # ここで適当な処理がしたい的な要望
            instance.item = Item.objects.all(pk=item_id)

        return super().update(instance, request)

DRFよくあるエラー

"detail": "Method \"PUT\" not allowed."
"detail": "Method \"PATCH\" not allowed."

request URL が list/ になってて、 list/1 のようにupdate対象のpkを指定していない

custom field を POST で受け取りたい場合

DRFのcreate() にくる request はvalidated_data なので、不要なdictは来ない
model/serializerにいない post requestを受け付けるには以下のunnecessaryな行が必要

class ItemSerializer(serializers.ModelSerializer):
    unnecessary = serializers.CharField(write_only=True)      # <-- ここに書くと request に含まれる対象として認識される
    class Meta:
        model = Item
        fields = ['id', 'name', 'type', 'unnecessary']      # <-------------


    def create(self, request):
        unnecessary = request.pop("unnecessary")      # <-- するとここにデータが入ってくる。modelsにはないfieldなので、 pop でrequestから削除するのが必須
        return Item.objects.create(**request)

fields / create / update ... の不思議

write_onlyを指定することによりこのフィールドをGET時には出さないようにしておきます。

fields にはget/post関わらず使う可能性のあるものをすべて書く。
postのときはあると困る・・とか考えなくていい。

write_only / read_only がそのスイッチで、DRFがいい感じに fields を解釈してくれる。

read_only = GET only
write_only = only POST, PATCH, PUT, DELETE と思えばよさそう

requestにやってくるデータの型はserializerのclass変数で来まる

CharField = str

class ItemSerializer(serializers.ModelSerializer):
    item_id = serializers.CharField(   #<---------- CharField = str()
        write_only=True,
    )

    class Meta:
        model = Item
        fields = ['id', 'name', 'tel']

    def create(self, request):
        item_id = request.get('item_id', None)   #<--- str で値が入る
        return Item.objects.create(job_history=item_id, **request)

UUIDField = uuid()

class ItemSerializer(serializers.ModelSerializer):
    item_id = serializers.UUIDField( #<------------- UUID
        write_only=True,
    )

    ...

    def create(self, request):
        item_id = request.get('item_id', None)   # uuid型になる
        ...

ハマった理由

fieldの指定をrelation系のものにすると、relationが見つからない場合は None になるようだ

class ItemSerializer(serializers.ModelSerializer):
    parrent_id = serializers.PrimaryKeyRelatedField( 
         とか
    parrent_id = serializers.StringsRelatedField( 
        とか
    ,,,

    def create(self, request):
        parrent_id = request.get('parrent_id', None)   # Noneになる
        ...

見つかった場合は、instance object が入る。

    def create(self, request):
        parrent = request.get('parrent_id', None)  
        print(parrent.pk)           # instanceの中に各fieldが入ってくる
        ...

serializer の relation を理解する

わからないこと: PrimaryKeyRelatedField って、pkとpkをつないだあと、何を返すの?
親を返すの?子を返すの? どっち?

manual

ここによると、PrimaryKeyRelatedField は親で使っている。子で使うとどうなるんだ?

親 → 子 relation

models.py
class Album(models.Model):
    album_name = models.CharField(max_length=100)
    artist = models.CharField(max_length=100)

class Track(models.Model):
    album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
    order = models.IntegerField()
    title = models.CharField(max_length=100)
    duration = models.IntegerField()

    class Meta:
        unique_together = ['album', 'order']
        ordering = ['order']

    def __str__(self):
        return '%d: %s' % (self.order, self.title)
serializers.py
class AlbumSerializer(serializers.ModelSerializer):
    tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

    class Meta:
        model = Album
        fields = ['album_name', 'artist', 'tracks']

class TrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = Track
        fields = ['order', 'title', 'duration']

input-data
python manage.py makemigrations
python manage.py migrate
python manage.py shell

import django
django.setup()

from myapp.models import *
from myapp.serializers import * 

album = Album.objects.create(album_name="The Grey Album", artist='Danger Mouse')
Track.objects.create(album=album, order=1, title='Public Service Announcement', duration=245)
Track.objects.create(album=album, order=2, title='What More Can I Say', duration=264)
Track.objects.create(album=album, order=3, title='Encore', duration=159)
serializer = AlbumSerializer(instance=album)
serializer.data
result-of-album
{
    "album_name": "The Grey Album",
    "artist": "Danger Mouse",
    "tracks": [
        1,
        2,
        3
    ]
}

子のpkが入りました。

子 → 親 relation

models.py
class AnotherAlbum(models.Model):
    album_name = models.CharField(max_length=100)
    artist = models.CharField(max_length=100)

class AnotherTrack(models.Model):
    album = models.ForeignKey(AnotherAlbum, related_name='tracks', on_delete=models.CASCADE)
    order = models.IntegerField()
    title = models.CharField(max_length=100)
    duration = models.IntegerField()

    class Meta:
        unique_together = ['album', 'order']
        ordering = ['order']

    def __str__(self):
        return '%d: %s' % (self.order, self.title)
serializers.py
class AnotherAlbumSerializer(serializers.ModelSerializer):
    #ここをやめて tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True) 

    class Meta:
        model = AnotherAlbum
        fields = ['album_name', 'artist'
            # , 'tracks'
        ]

class AnotherTrackSerializer(serializers.ModelSerializer):
    album = serializers.PrimaryKeyRelatedField(read_only=True) # 子に追加
    class Meta:
        model = AnotherTrack
        fields = ['order', 'title', 'duration', 'album']
input-data
python manage.py makemigrations
python manage.py migrate
python manage.py shell

import django
django.setup()

from myapp.models import *
from myapp.serializers import * 

album = AnotherAlbum.objects.create(album_name="The Grey Album", artist='Danger Mouse')
AnotherTrack.objects.create(album=album, order=1, title='Public Service Announcement', duration=245)
AnotherTrack.objects.create(album=album, order=2, title='What More Can I Say', duration=264)
AnotherTrack.objects.create(album=album, order=3, title='Encore', duration=159)
serializer = AnotherAlbumSerializer(instance=album)
serializer.data
result-of-track
[
    {
        "order": 1,
        "title": "Public Service Announcement",
        "duration": 245,
        "album": 1
    },
    {
        "order": 2,
        "title": "What More Can I Say",
        "duration": 264,
        "album": 1
    },
    {
        "order": 3,
        "title": "Encore",
        "duration": 159,
        "album": 1
    }
]

親のpkが入りました。

結論

PrimaryKeyRelatedField は 親に使えば子のpkを返すし、子に使えば親のpkを返す。

結果、どうなったか

relationしてる子をcreateするとき、parent objectを取るのをやめた。
create() では UUIDField でただの変数として取得。
その値を使って親のobjectを取得、 Item.object.create(parent=parent, **request) するようにした。

Noneになるよってエラーより、このほうがjoin先のpkが見つからないエラーもわかりやすかった。