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

Posted

技术标签:

【中文标题】Django Rest Framework - 使用 ModelSerializer 和 ModelViewSet 更新相关模型【英文标题】:Django Rest Framework - Updating related model using ModelSerializer and ModelViewSet 【发布时间】:2019-04-26 16:18:56 【问题描述】:

背景

我有两个序列化器: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')

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

 class PostViewSet(viewsets.ModelViewSet):
        queryset = Post.objects.all()
        serializer_class = PostSerializer

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

目标

目标很简单。

通过 PostMan 发送 PUT 请求,其 url 类似于“PUT http://localhost:8000/api/posts/1/” 由于应包含图像文件,因此请求将通过below. 等表单数据完成

问题

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

“相片”: [ “这是必填栏。” ], “标题”: [ “这是必填栏。” ], “内容”: [ “这是必填栏。” ]

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

很明显,我的 PUT 字段都没有应用。 所以我一直在挖掘 Django rest 框架源代码本身,并在 ViewSet update() method continues to fail 中发现了序列化程序验证。

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

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

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

谢谢。

更新

正如 Neil 所指出的,我在 PostSerializer 的 update() 方法的第一行添加了 print(self)。但是,我的控制台上没有打印任何内容。

我认为这是由于我上面的怀疑,因为调用序列化程序 update() 方法的 perform_update() 方法被称为 AFTER serializer is validated。

因此,我的问题的主要概念可以缩小到以下几点。

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

再次感谢。

【问题讨论】:

这三个字段都发生了失败。因此,也许可以尝试禁用图像,然后将文本和内容与 json put 一起放置,看看是否有效。如果确实如此,那么至少您知道问题出在哪里。 您应该尝试的另一件事是在更新方法的顶部注销(打印)经过验证的数据。这将告诉你是否有任何事情通过。 @Neil 感谢您的回复 Neil。我已经要求不发送图像并且它有效。没有发生验证错误并且数据设置正确。关于您的第二条评论,请查看我的更新。 哦,那么问题出在图像/表单数据上。我以前没有处理过,所以我不能说你应该做什么。但也许看看文档的解析器部分:django-rest-framework.org/api-guide/parsers/#multipartparser @Neil 哦,我还没想过解析器。我想这将是一个很好的起点。如果有进展,我会做后续评论。再次感谢您的回复。 【参考方案1】:

首先你需要设置标题:

Content-Type: multipart/form-data;

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

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

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

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

将视图集中的 update 方法编辑为部分更新序列化程序 编辑路由器以始终在更高级的序列化程序中调用 partial_update 方法

您可以覆盖视图集 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)

添加

rest_framework.parsers.MultiPartParser

到 REST_FRAMEWORK 字典的主设置文件:

REST_FRAMEWORK = 
    ...
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework.parsers.JSONParser',
        'rest_framework.parsers.MultiPartParser',
    )

看看你的序列化器,很奇怪你没有从 PostSerializer 得到错误,因为你没有向 Meta.fields 元组添加“照片”字段。

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

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

【讨论】:

这个序列化程序设置会遇到更多问题。 感谢您的详细解决方案@mon。似乎我在简化问题时错过了“照片”字段。我正在研究您提供的解决方案。完成后我会提供彻底的跟进。再次感谢。【参考方案2】:

解决方案有点混合或@Neil 和@mon 的答案。不过我会更直白一点。

分析

现在 Postman 提交的表单数据包含 2 个键值对(请参阅我在原始问题中上传的照片)。一个是“照片”键域与多个照片文件相关联,另一个是“数据”键域与一大块 'JSON-like string' 相关联。尽管这是一种将数据与文件一起 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'",]
>

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

但是我的 PostSerializer 期望 request.data 看起来像下面这样。

 "photos": ["image": ImageObject1, "post":1, "image": ImageObject2, "post":2, ... ],
  "title": "test title",
  "content": "test content"
 

因此,让我们做一些实验,按照上面的 JSON 格式 PUT 一些数据。 即

 "photos": ["image": "http://tny.im/gMU", "post": 1],
  "title" : "test title",
  "content": "test content"

您将收到如下错误消息。

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

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

现在整个问题的原因变得清晰了。需要修复的是 Parser,以便 Serializer 可以理解提交的表单数据。

解决方案

1.编写新的解析器

新解析器应将“类 JSON”字符串解析为正确的 JSON 数据。我从here.借来了MultipartJSONParser

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

class MultipartJsonParser(parsers.MultiPartParser):
    # https://***.com/a/50514022/8897256
    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)

2。重新设计序列化器

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

    覆盖 ModelViewSet 的 updatemethod 从request.data 中弹出“照片”键值对 将弹出的“照片”对翻译成包含“图像”和“发布”键的字典列表。 将结果附加到request.data,并带有键名“photos”。这是因为我们的 PostSerializer 期望键名是“photos”。

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

只需这样做:

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


class PostImageSerializer(serializer.ModelSerializer):
    class Meta:
        model = PostImage
        fields = '__all__'

3.从 ModelViewSet 覆盖 update() 方法

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

最后,感谢我们新设计的 Parser,普通的“JSON-like”字符串将被转换为常规 JSON 数据。所以只需简单地将所有内容放入serializer_classperform_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)

结论

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

【讨论】:

以上是关于Django Rest Framework - 使用 ModelSerializer 和 ModelViewSet 更新相关模型的主要内容,如果未能解决你的问题,请参考以下文章

Django rest framework JWT ,删除 jwt 令牌

django-rest-framework - 在可浏览的 API 中自动生成表单?

Django后端开发学习笔记Django REST Framework基于类的视图

Django后端开发学习笔记Django REST Framework基于类的视图

POST 请求创建多个对象 AJAX/Django Rest Framework

Django Rest Framework 序列化程序中的循环依赖