Django Rest Framework - 使用 ModelSerializer 和 ModelViewSet 更新相关模型

Ihn*_*Kim 5 python django django-rest-framework

背景

我有两个序列化器:PostSerializerPostImageSerializer,它们都继承了 DRF ModelSerializer。PostImage 模型通过 related_name='photos' 与 Post 相关联。

由于我希望序列化程序执行update,PostSerializer 覆盖 ModelSerializer 中的 update() 方法,如官方 DRF 文档中所述。

class PostSerializer(serializers.ModelSerializer):
    photos = PostImageSerializer(many=True)

    class Meta:
        model = Post
        fields = ('title', 'content')

    def update(self, instance, validated_data):
        photos_data = validated_data.pop('photos')
        for photo in photos_data:
            PostImage.objects.create(post=instance, image=photo)
        return super(PostSerializer, self).update(instance, validated_data)

class PostImageSerializer(serializer.ModelSerializer):
    class Meta:
        model = PostImage
        fields = ('image', 'post')
Run Code Online (Sandbox Code Playgroud)

我还定义了一个继承ModelViewSet 的 ViewSet

 class PostViewSet(viewsets.ModelViewSet):
        queryset = Post.objects.all()
        serializer_class = PostSerializer
Run Code Online (Sandbox Code Playgroud)

最后 PostViewSet 被注册到 DefaultRouter。(省略代码)

目标

目标很简单。

问题

我收到 400 响应,错误消息如下。

{
"照片": [ "此字段是必需的。" ],
"title": [ "此字段为必填项。" ],
"content": [ "此字段为必填项。" ]
}

(您是否应该注意错误消息可能与 DRF 错误消息不完全吻合,因为它们已被翻译。)

很明显,我的 PUT 字段都没有被应用。所以我一直在挖掘 Django rest 框架源代码本身,并发现 ViewSet update() 方法中的序列化程序验证仍然失败

我对此表示怀疑,因为我不是通过 JSON 而是通过使用键值对的表单数据 PUT 请求,所以 request.data 没有得到正确验证。

但是,我应该在请求中包含多个图像,这意味着纯 JSON 不起作用。

对于这种情况,最明确的解决方案是什么?

谢谢你。

更新

正如尼尔指出的那样,我在 PostSerializer 的 update() 方法的第一行添加了 print(self)。但是我的控制台上没有打印出来。

我想这是因为我的doupt上述这就要求串行update()方法被调用,因为perform_update()方法 串行进行了验证

因此,我的问题的主要概念可以缩小为以下内容。

  1. 我应该如何修复请求的数据字段,以便 ModelViewSet 的 update() 方法中的验证可以通过?
  2. 我是否必须覆盖 ModelViewSet 的 update() 方法(不是 ModelSerializer 中的方法)?

再次感谢。

小智 6

首先你需要设置标题:

Content-Type: multipart/form-data;
Run Code Online (Sandbox Code Playgroud)

但也许如果你在邮递员中设置表单数据,这个标题应该是默认的。

您无法将图像作为 json 数据发送(除非您将其编码为字符串并在服务器端解码为图像,例如 base64)。

在 DRF PUT中,默认需要所有字段。如果您只想设置部分字段,则需要使用PATCH

要解决此问题并使用PUT更新部分字段,您有两种选择:

  • 编辑视图集中的更新方法以部分更新序列化器
  • 编辑路由器以始终调用序列化器中的partial_update方法,这是更高级的

您可以重写视图集更新方法以始终更新序列化器部分(仅更改提供的字段):

    def update(self, request, *args, **kwargs):
        partial = True # Here I change partial to True
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        return Response(serializer.data)
Run Code Online (Sandbox Code Playgroud)

添加

rest_framework.parsers.MultiPartParser

到 REST_FRAMEWORK 字典的主设置文件:

REST_FRAMEWORK = {
    ...
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.MultiPartParser',
    )
}
Run Code Online (Sandbox Code Playgroud)

查看您的序列化程序,很奇怪您没有从PostSerializer收到错误,因为您没有将“照片”字段添加到 Meta.fields 元组中。

在这种情况下我的更多建议:

  • required=False添加到您的照片字段(除非您希望这是必需的)
  • 如上所述,将照片字段添加到 Meta.fields tuple fields = ('title', 'content', 'photos',)
  • 为您的valid_data.pop('photos')添加默认None值,然后检查循环之前提供的照片数据。


Ihn*_*Kim 2

解决方案有点混合或@Neil 和@mon 的答案。不过我会再澄清一点。

分析

现在,Postman 提交的表单数据包含 2 个键值对(请参阅我在原始问题中上传的照片)。一个是与多个照片文件链接的“照片”键字段,另一个是与一大块“类似 JSON 的字符串”链接的“数据”键字段。尽管这是一种将数据与文件一起 POST 或 PUT 的公平方法,但 DRF MultiPartParser 或 JSONParser 无法正确解析这些数据。

我收到错误消息的原因很简单。self.get_serializer(instance, data=request.data, partial=partial里面的方法ModelViewSet(特别是UpdateModelMixin)无法理解request.data部分。

目前request.data提交的表单数据如下所示。

<QueryDict: { "photos": [PhotoObject1, PhotoObject2, ... ],
  "request": ["{'\n 'title': 'title test', \n 'content': 'content test'}",]
}>
Run Code Online (Sandbox Code Playgroud)

仔细观察“请求”部分。该值是一个普通string对象。

然而我的 PostSerializer 希望request.data看起来像下面这样。

{ "photos": [{"image": ImageObject1, "post":1}, {"image": ImageObject2, "post":2}, ... ],
  "title": "test title",
  "content": "test content"
 }
Run Code Online (Sandbox Code Playgroud)

因此,我们来做一些实验,按照上面的 JSON 形式 PUT 一些数据。IE

{ "photos": [{"image": "http://tny.im/gMU", "post": 1}],
  "title" : "test title",
  "content": "test content"
}
Run Code Online (Sandbox Code Playgroud)

您将收到如下错误消息。

"photos": [{"image": ["提交的数据不是文件。"]}]

这意味着每个数据都已正确提交,但图像 url http://tny.im/gMU不是文件而是字符串。

现在整个问题的原因已经清楚了。需要修复解析器,以便序列化器可以理解提交的表单数据。

解决方案

1. 编写新的解析器

新的解析器应该将“类 JSON”字符串解析为正确的 JSON 数据。我从这里借用了 MultipartJSONParser

这个解析器的作用很简单。如果我们提交带有键“data”的“类 JSON”字符串,则从jsonrest_framework 调用并解析它。之后,返回解析后的 JSON 和请求的文件。

class MultipartJsonParser(parsers.MultiPartParser):
    # /sf/answers/3535981571/
    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = {}
        data = json.loads(result.data["data"])
        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)
Run Code Online (Sandbox Code Playgroud)

2.重新设计序列化器

官方 DRF 文档建议使用嵌套序列化器来更新或创建相关对象。然而,我们有一个显着的缺点,即 InMemoryFileObject 无法转换为序列化器期望的正确形式。为此,我们应该

  1. ModelViewSet 的重写update方法
  2. 弹出“照片”键值对request.data
  3. 将弹出的“照片”对翻译成包含“图像”和“发布”键的字典列表。
  4. 将结果附加到request.data键名“photos”。这是因为我们的 PostSerializer 期望键名是“photos”。

但基本上request.data是一个默认情况下不可变的 QuerySet。我非常怀疑我们是否必须强制改变 QuerySet。因此,我宁愿将 PostImage 创建过程委托update()ModelViewSet. 在这种情况下,我们就不需要nested serializer再定义了。

只需这样做:

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = '__all__'


class PostImageSerializer(serializer.ModelSerializer):
    class Meta:
        model = PostImage
        fields = '__all__'
Run Code Online (Sandbox Code Playgroud)

3. 重写update()方法ModelViewSet

为了利用我们的 Parser 类,我们需要显式指定它。我们将整合 PATCH 和 PUT 行为,因此设置partial=True。正如我们之前看到的,图像文件带有“照片”键,因此弹出值并创建每个照片实例。

最后,得益于我们新设计的解析器,普通的“类 JSON”字符串将被转换为常规 JSON 数据。所以只需将所有内容放入serializer_class和即可perform_update

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    # New Parser
    parser_classes = (MultipartJsonParser,)

    def update(self, request, *args, **kwargs):
        # Unify PATCH and PUT
        partial = True
        instance = self.get_object()

        # Create each PostImage
        for photo in request.data.pop("photos"):
            PostImage.objects.create(post=instance, image=photo)

        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        # Do ViewSet work.
        self.perform_update(serializer)
        return Response(serializer.data)
Run Code Online (Sandbox Code Playgroud)

结论

该解决方案有效,但我不确定这是保存外键相关模型的最干净的方法。我强烈的感觉是序列化器应该保存相关模型。正如文档所述,文件以外的数据都是以这种方式保存的。如果有人能告诉我更微妙的方法来做到这一点,我将不胜感激。