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

时间:2018-11-24 10:06:32

标签: python django django-rest-framework

背景

我有两个序列化器: PostSerializer PostImageSerializer ,它们都继承了DRF ModelSerializer。 PostImage模型通过related_name ='photos'与Post链接。

由于我希望序列化程序执行 update ,因此PostSerializer会按照DRF官方文档中的说明覆盖ModelSerializer的update()方法。

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。 (省略的代码)

目标

目标很简单。

问题

我得到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

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

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

再次感谢。

2 个答案:

答案 0 :(得分:2)

首先,您需要设置标题:

Content-Type: multipart/form-data;
  

但是,如果您在邮递员中设置表单数据,则此标头应为   默认。

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

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

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

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

您可以覆盖viewset 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 (除非您希望这样做)
  • 如上所述,
  • 向您添加照片字段。Meta.fields元组 fields =('title','content','photos',)
  • 为您的 validated_data.pop('photos')添加默认的值,然后在循环之前检查是否提供了照片数据。

答案 1 :(得分:1)

解决方案有些混合,或者是@Neil和@mon的答案。但是,我会理顺一下。

分析

现在,邮递员提交的表单数据包含2个键值对(请参阅我在原始问题中上传的照片)。一个是与多个照片文件链接的“照片”关键字段,另一个是与“类似于JSON的字符串” 的一大块链接的“数据”关键字段。尽管这是一种与文件一起发布或放置数据的公平方法,但是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格式放入一些数据。 即

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

您将收到以下错误消息。

  

“照片”:[{“图像”:[“提交的数据不是文件。”]}]

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

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

解决方案

1。编写新的解析器

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

借了MultipartJSONParser

此解析器的作用很简单。如果我们使用键“ data”提交“类似于JSON”的字符串,请从rest_framework调用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。这是因为我们的PostSerializer期望键名是“照片”。

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

只需执行以下操作:

nested serializer

3。从class PostSerializer(serializers.ModelSerializer): class Meta: model = Post fields = '__all__' class PostImageSerializer(serializer.ModelSerializer): class Meta: model = PostImage fields = '__all__' 覆盖update()方法

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

最后,由于我们新设计了解析器,将简单的“类似于JSON”字符串转换为常规JSON数据。因此,只需简单地将partial=Trueserializer_class中的内容放进去。

perform_update

结论

该解决方案有效,但是我不确定这是保存与外键相关的模型的最干净的方法。我强烈感觉是应该保存相关模型的是串行器。就像文档中指出的那样,文件以外的数据都以这种方式保存。如果有人可以告诉我更微妙的方法,那么我将不胜感激。