Django-Rest框架使用ModelSerializer和ModelVi更新相关模型

2024-10-01 13:39:19 发布

您现在位置:Python中文网/ 问答频道 /正文

背景

我有两个序列化程序:PostSerializerPostImageSerializer,它们都继承了DRF ModelSerializer。PostImage模型通过相关的_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的视图集。在

^{pr2}$

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

目标

目标很简单。在

问题

我得到400个响应,错误消息如下。在

{
"photos": [ "This field is required." ],
"title": [ "This field is required." ],
"content": [ "This field is required." ]
}

(请注意,错误消息可能与DRF错误消息不完全匹配,因为它们是经过翻译的。)

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

我对此表示怀疑,因为我不是通过JSON而是通过使用键值对的表单数据来提交请求请求数据未正确验证。在

但是,我应该在请求中包含多个图像,这意味着普通JSON无法工作。在

对于这个案子,最清楚的解决办法是什么?在

谢谢。在

更新

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

我认为这是由于我上面的错误,因为调用serializer update()方法的perform_pdate()方法在serializer is validated之后被调用。在

因此,我的问题的主要概念可以缩小到以下几个方面。在

  1. 如何修复请求的数据字段,以便ModelViewSet的update()方法内的验证可以通过?在
  2. modelviewI()的modelviewer()方法没有覆盖?在

再次感谢。在


Tags: 方法selfdata序列化is错误updatedrf
2条回答

首先需要设置标题:

Content-Type: multipart/form-data;

But maybe if you set form-data in postman, this header should be default.

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

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

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

  • 将viewset中的update方法编辑为部分更新序列化程序
  • 编辑路由器,使其始终在序列化程序中调用部分更新方法,这是更高级的方法

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

^{pr2}$

添加

休息_framework.parsers.MultiPartParser

到REST_FRAMEWORK dict的主设置文件:

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

看看你的序列化程序,奇怪的是你没有从后序列化程序中得到错误,因为你没有添加“photos”字段到元字段元组。在

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

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

答案是某种程度上的混合,或者是尼尔和蒙的答案。不过,我会再理顺一点。在

分析

现在邮递员提交包含2个键值对的表单数据(请参阅我在原始问题中上传的照片)。一个是与多个照片文件链接的“photos”键字段,另一个是“data”键字段,它与一个'JSON-like string'链接在一起。尽管这是一种将数据与文件一起发布或放置的公平方法,但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看起来像下面这样。在

^{pr2}$

因此,让我们做一些实验,并按照上面的JSON表单放置一些数据。 i、 e

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

您将收到如下错误消息。在

"photos": [{"image": ["submitted data is not a file."]}]

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

现在这整个问题的原因变得很清楚了。序列化程序可以理解它需要提交的数据格式。在

解决方案

1。编写新的解析器

新的解析器应该将'JSON-like'字符串解析为正确的JSON数据。我从here.借用了MultipartJSONParser

这个解析器所做的很简单。如果我们提交带有键'data'的'JSON-like'字符串,请从rest\u框架调用json并对其进行解析。然后,返回解析后的JSON和请求的文件。在

class MultipartJsonParser(parsers.MultiPartParser):
    # https://stackoverflow.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不能转换成序列化程序所期望的正确格式。为此,我们应该

  1. 重写ModelViewSet的update方法
  2. request.data弹出“照片”键值对
  3. 将弹出的“照片”对翻译成包含“image”和“post”键的词典列表。在
  4. 将结果附加到request.data中,密钥名为“photos”。这是因为我们的PostSerializer要求密钥名为“photos”。在

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

只需这样做:

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


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

3。从ModelViewSet重写update()方法

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

最后,由于我们新设计的解析器,普通的“类JSON”字符串将被转换为常规JSON数据。所以只要简单地把每件事都放进serializer_class和{}。在

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)

结论

解决方案可行,但我不确定这是保存外键相关模型的最干净的方法。我强烈感觉应该是序列化程序保存相关模型。正如文件所述,除了文件以外的数据都是这样保存的。如果有人能告诉我更巧妙的方法,我将不胜感激。在

相关问题 更多 >