django REST framework 嵌套序列化器和 POST 嵌套 JSON 文件

Posted

技术标签:

【中文标题】django REST framework 嵌套序列化器和 POST 嵌套 JSON 文件【英文标题】:django REST framework nested serializer and POST nested JSON with files 【发布时间】:2018-05-19 12:36:06 【问题描述】:

如何将包含文件(图像)在内的嵌套数据的 POST 请求发送到 django REST 嵌套序列化程序?

给定这个 JS 对象:

bookData: 
    title: 'Anne of Green Gables',
    coverImage: File(123456),
    pages: 123,
    author: 
        name: 'Lucy Maud Montgomery',
        born: 1874,
        profilepicture_set: [
            file: File(234567), description: 'Young L. M. Montgomery',
            file: File(234568), description: 'Old L. M. Montgomery
        ],
        quote_set: [
            text: "I'm so glad I live in a world where there are Octobers.",
            text: "True friends are always together in spirit.",
        ]
    ,


我想向我的 django REST API 发送一个 POST 请求(我在前端使用 VueJS,但这并不重要)。

# views.py
class CreateBookView(generics.CreateAPIView):
    serializer_class = CreateBookSerializer
    queryset = Book.objects.all()

# serializers.py
class CreateBookSerializer(serializers.ModelSerializer):
    author = CreateAuthorSerializer()

    class Meta:
        model = Book
        fields = ('title', 'pages', 'author')

    @transaction.atomic
    def create(self, validated_data):
        author_data = validated_data.pop('author')
        uploader = self.context['request'].user
        book = Book.objects.create(uploader=uploader, **validated_data)

        Author.objects.create(book=book, **author_data)

        return book


# models.py
class AuthorManager(models.Manager):

    def create(self, **author_data):
        quotes_data = author_data.pop('quote_set')
        photos_data = author_data.pop('profilepicture_set')

        author = Author(**author_data)
        author.save()

        for quote_data in quotes_data:
            Quote.objects.create(author=author, **quote_data)

        for photo_data in photos_data :
            ProfilePicture.objects.create(author=author, **photo_data)

        return author

## Skipping the CreateAuthorSerializer, Quote and ProfilePicture Managers as they are analogous.
## Assume one Author can only write one Book (OneToOneField). No need to look the name up for existing Authors.

编辑

我想添加一些关于如何在 VueJS 的前端发送数据的信息。

sendData()
    var fd = new FormData()
    var bookData = 
        title: this.$store.getters.title, # 'Anne of Green Gables"
        coverImage: this.$store.getters.coverImage, # File(somesize)
        pages: this.$store.getters.pages, # 123
        author: this.$store.getters.author, # nested object 
        ...
    
    fd = objectToFormData(bookData, fd) # external function, see below
    this.$http.post('api/endpoint/create/', fd, headers: 
        'Content-Type': 'multipart/form-data',
        'X-CSRFToken': Cookies.get('csrftoken'),
        
    ).then(...)

sendData() 在按钮单击时被调用。 objectToFormData 是来自 npm (click) 的外部库,可将嵌套对象转换为平面形式。

这个解决方案(以及@Saji Xavier 的解决方案)的问题是request.data 包含一个丑陋的QueryDict,它与原始bookData 对象结构相去甚远,并且不被REST 序列化程序(即author 字段丢失 - 这是真的,对象已被展平)。

<QueryDict: 
'title': ['Anne of Green Gables'], 
'coverImage':[InMemoryUploadedFile: cover.jpg (image/jpeg)],
'pages': ['123'], 
'name': ['Lucy Maud Montgomery'], 
'born': ['1874'],
'profilepicture_set[][description]': ['Young L. M. Montgomery', 'Old L. M. Montgomery'], 
'profilepicture_set[][file]': [
    InMemoryUploadedFile: young.jpg (image/jpeg), 
    InMemoryUploadedFile: old.jpg (image/jpeg)
    ],
'quote_set[][text]': [
    "I'm so glad I live in a world where there are Octobers.",
    "True friends are always together in spirit."
    ]
>

我现在该如何处理?好像太丑了我能想到的是覆盖create()函数。

class CreateBookView(generics.CreateAPIView):
    (...)
    def create(self, request, *args, **kwargs):
        book_data = 
        book_data['title'] = request.data['title']
        book_data['pages'] = request.data['pages']
        book_data['cover_image'] = request.data['coverImage']
        book_data['author'] = 
        book_data['author']['name'] = request.data['name']
        book_data['author']['born'] = request.data['born']
        (...)
        serializer = self.get_serializer(data=book_data)
        (...)

这是一个极其丑陋的解决方案,对于具有多个字段和多个嵌套级别的模型无效(本书/作者只是一个玩具示例)。

我应该如何处理?我是改变 POST 的方式(以获得漂亮的request.data 格式)还是处理这个丑陋的?

【问题讨论】:

【参考方案1】:

根据 DRF documentation,

大多数解析器,例如 e.g. JSON 不支持文件上传。姜戈的 常规 FILE_UPLOAD_HANDLERS 用于处理上传的文件。

一种选择是使用 javascript FormData API 发送多部分数据(适用于 VueJS/AngularJS)

var fd = new FormData();
fd.append('title', 'Anne of Green Gables')
fd.append('author_name', 'Lucy Maud Montgomery')
fd.append('author_profilepicture_set', files[])
$http.post('url', fd, 
   headers: 
       'Content-Type': 'multipart/form-data'
   
);

【讨论】:

这并没有解决我的问题 - 我设法发送了数据,但是请求格式对于 DRF 序列化程序的处理来说非常不舒服。 multipart 会给序列化程序带来问题,所以我的建议是不要使用序列化程序功能,如(验证、保存等),而是使用 request.POST.get(data) 并将文件保存在服务器中(或单独处理文件,让序列化程序处理其余数据) 我知道这不是更清洁的解决方案,当我在寻找相同的解决方案时找不到任何其他解决方案,我们必须采用 FormData 方法。 是的,我知道我可以让它这样工作,我只是......这样做感觉不好 :) 容易出错,模型更新后容易出现不兼容......呃: X

以上是关于django REST framework 嵌套序列化器和 POST 嵌套 JSON 文件的主要内容,如果未能解决你的问题,请参考以下文章

需要 Django Rest Framework 嵌套序列化程序 = False 错误

Django REST Framework:嵌套序列化程序未出现

在 Django REST Framework 中显示嵌套实体

django-rest-framework:如何更新嵌套的外键?我的更新方法甚至没有被调用

如何通过 Django Rest Framework 返回嵌套的 json

在 Django REST Framework 中处理嵌套对象表示