【DRF】drf-writable-nestedはvalidated_dataの値を参照しない
はじめに
Django REST frameworkで、リレーションされたデータを生成しようと考えた時、公式ドキュメントには
と示されています。ふむ。
また、他の選択肢としては以下のサードパーティライブラリがあります
前者・後者ともやりたいことは実現できる為、drf-writable-nestedを採用した結果、リクエストで受け取った値以外を含めての作成・更新に苦労しましたってお話しです
そもそも何をしたかったの?
上記のdrf-writable-nestedを用いて、リレーション先のデータ作成・更新を行う際に、serializerで値を変更して作成・更新を行いたかったのですが
drf-writable-nestedはリレーション先のデータを作成・更新する際には渡されるvalidated_dataではなくinitial_dataを見て作成・更新する為どちゃめちゃに嵌ってしまいました
どんな感じなのか今回は実際にBookとCommonDetailを作ったため、それを踏まえて、備忘録として残します
環境
- python 3.11.4
- django 4.2.1
- drf 3.14.0
- drf-writable-nested 0.7.0
ベースとなる諸々
Book・CommonDetailのModel・Serialiser・ViewSetを定義します
model
class Book(models.Model):
title = models.CharField("名前", max_length=256)
page_size = models.PositiveSmallIntegerField("ページ数")
version = models.PositiveSmallIntegerField("版")
class CommonDetail(models.Model):
description = models.TextField("説明", blank=True, null=True)
creator = models.CharField("作者", max_length=256)
book = models.OneToOneField(
"api.Book",
on_delete=models.CASCADE,
verbose_name="本",
related_name="detail",
blank=True,
null=True,
)
BookとCommonDetailは1to1で繋がっています
serializer
続いてSerializer(WritableNestedModelSerializerでの実装は後ほど)
class CommonDetailSerializer(serializers.ModelSerializer):
class Meta:
model = CommonDetail
fields = (
"id",
"description",
"creator",
)
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = (
"id",
"title",
"page_size",
"version",
)
extra_kwargs = {
"version": {
"read_only": True,
},
}
def create(self, validated_data):
validated_data["version"] = Book.objects.filter(title__icontains=validated_data.get("title")).count()
return super().create(validated_data)
Bookのversionはread_onlyで定義し、createをoverrideしserializerにて値を設定するようにしています
(これが後の混乱を生むミソpointでした...)
viewset
最後にViewSetです
class BookAPIViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
queryset = Book.objects.all()
一旦ここまででgetとpostを試してみようと思いまっす
$ curl -w'\n' -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/api/books/ -d '{"title": "test", "page_size": "100"}'
{"id":1,"title":"test","page_size":100,"version":0}
$ curl -w'\n' http://127.0.0.1:8000/api/books/
{"count":1,"next":null,"previous":null,"results":[{"id":1,"title":"test","page_size":100,"version":0}]}
serializerで値を埋め込んでいるversionもしっかりと反映されていて感じですね
全体のディレクトリ構成はこんな感じです
├── api
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── serializers.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── config
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
ここからdetailのdescriptionに対して、値がなかったらtitleの値を反映したいと思います
drf-writable-nestedを適用
まずは特殊なことはせずに使用してみようと思います
serializerを以下の通りに変更します
class BookSerializer(WritableNestedModelSerializer):
detail = CommonDetailSerializer()
class Meta:
model = Book
fields = (
"id",
"title",
"page_size",
"version",
"detail",
)
extra_kwargs = {
"version": {
"read_only": True,
},
}
...
これでBookを作成・更新する際にdetailも一緒に作成・更新されるようになりました
確認してみましょー
$ curl -w'\n' -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/api/books/ -d '{"title": "test", "page_size": "100", "detail": {"description": "test description" , "creator": "user"}}'
{"id":2,"title":"test","page_size":100,"version":1,"detail":{"id":1,"description":"test description","creator":"user"}}
$ curl -w'\n' http://127.0.0.1:8000/api/books/ | python -mjson.tool
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"title": "test",
"page_size": 100,
"version": 0,
"detail": null
},
{
"id": 2,
"title": "test",
"page_size": 100,
"version": 1,
"detail": {
"id": 1,
"description": "test description",
"creator": "user"
}
}
]
}
しっかりとdetailも作成されていますねいい感じってやつです
では本題の奴行きましょう
ダメな例
まず一番安直にversionと同じようにvalidated_dataに埋め込んで作成を試してみます
class BookSerializer(WritableNestedModelSerializer):
...
def create(self, validated_data):
validated_data["version"] = Book.objects.filter(title__icontains=validated_data.get("title")).count()
validated_data["detail"].setdefault("description", validated_data["title"])
return super().create(validated_data)
$ curl -w'\n' -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/api/books/ -d '{"title": "test", "page_size": "100", "detail": {"creator": "user"}}'
{"id":3,"title":"test","page_size":100,"version":2,"detail":{"id":2,"description":null,"creator":"user"}}
が、ダメ!地獄いきぃ
というのも、drf-writable-nestedはvalidated_dataを見ているのではなくinitial_dataを見てリレーションデータを作成しているからです
このため、埋め込むのであればvalidated_dataではなくinitial_dataに埋め込む必要があります
良い例
では実際initial_dataに適用したいと思います
class BookSerializer(WritableNestedModelSerializer):
...
def create(self, validated_data):
validated_data["version"] = Book.objects.filter(title__icontains=validated_data.get("title")).count()
# validated_data["detail"].setdefault("description", validated_data["title"])
self.initial_data["detail"].setdefault("description", validated_data["title"])
return super().create(validated_data)
$ curl -w'\n' -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/api/books/ -d '{"title": "test", "page_size": "100", "detail": {"creator": "user"}}'
{"id":4,"title":"test","page_size":100,"version":3,"detail":{"id":3,"description":"test","creator":"user"}}
うまくdescriptionがtitleの値で更新されてますね
その他
上記ともう一点リレーション先の作成・更新で気を付けるべき箇所があります
それは、get_initialはread_onlyを対象外にしてしまうことです
そのため、いくらinitial_dataに埋め込んだとしても値は更新されません
先ほどのdescriptionをread_onlyにして、titleを埋め込むだけにしてみましょう
class CommonDetailSerializer(serializers.ModelSerializer):
class Meta:
model = CommonDetail
fields = (
"id",
"description",
"creator",
)
extra_kwargs = {
"description": {
"read_only": True,
},
}
$ curl -w'\n' -X POST -H "Content-Type: application/json" http://127.0.0.1:8000/api/books/ -d '{"title": "test", "page_size": "100", "detail": {"creator": "user"}}'
{"id":5,"title":"test","page_size":100,"version":4,"detail":{"id":4,"description":null,"creator":"user"}}
この通り先ほどと全く同じリクエストですがdescriptionがnullとなっています
まとめ
drf-writable-nestedとしてはinitial-dataを使うこと自体は仕様のようなのですが
パッと見の挙動等ものすごくわかりずらかったので混乱しました...
(元々Bookのversion部分はvalidated_dataをserializerで値を埋め込んでいたことも拍車をかけました)
ここまで大変ならcreate, update時に独自でやっちゃったほうが楽だったりしそう感はありました(結局上記で実装しましたが)
他もっとよい方法等ありましたらコメントで教えていただけると僕が助かりまっす
ソースコードはgithubにあげてありますのでよろしければ
PS
気づいたら8月になっていて、前回個人ブログで記事を書いてから丸一年経っていました...
またちょくちょくこういった記事を書いていけたらなぁと思います
(気分一新zennを使って記事書いていこうかなと思います)
Discussion