【问题标题】:How to access serializer.data on ListSerializer parent class in DRF?如何访问 DRF 中 ListSerializer 父类的 serializer.data?
【发布时间】:2018-01-13 21:41:13
【问题描述】:

我在尝试访问serializer.data 时遇到错误,然后在Response(serializer.data, status=something) 中返回它:

尝试获取序列化程序 <serializer> 上字段 <field> 的值时出现 KeyError。

这发生在所有字段上(因为事实证明我正在尝试访问父级而不是子级的.data,见下文)

类定义如下所示:

class BulkProductSerializer(serializers.ModelSerializer):

    list_serializer_class = CustomProductListSerializer

    user = serializers.CharField(source='fk_user.username', read_only=False)

    class Meta:
        model = Product
        fields = (
            'user',
            'uuid',
            'product_code',
            ...,
        )

CustomProductListSerializer 是一个serializers.ListSerializer 并具有一个重写的save() 方法,允许它正确处理批量创建和更新。

这是来自批量产品ViewSet 的示例视图:

def partial_update(self, request):

    serializer = self.get_serializer(data=request.data,
                        many=isinstance(request.data, list),
                        partial=True)
    if not serializer.is_valid():
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    serializer.save()
    pdb.set_trace()
    return Response(serializer.data, status=status.HTTP_200_OK)

尝试在跟踪(或后面的行,显然)处访问serializer.data 会导致错误。这是完整的跟踪(tl;dr 跳过下面我用调试器诊断的地方):

 Traceback (most recent call last):
  File "/lib/python3.5/site-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/lib/python3.5/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/lib/python3.5/site-packages/rest_framework/viewsets.py", line 86, in view
    return self.dispatch(request, *args, **kwargs)
  File "/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch
    response = self.handle_exception(exc)
  File "/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch
    response = handler(request, *args, **kwargs)
  File "/application/siop/views/API/product.py", line 184, in partial_update
    return Response(serializer.data, status=status.HTTP_200_OK)
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 739, in data
    ret = super(ListSerializer, self).data
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 265, in data
    self._data = self.to_representation(self.validated_data)
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 657, in to_representation
    self.child.to_representation(item) for item in iterable
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 657, in <listcomp>
    self.child.to_representation(item) for item in iterable
  File "/lib/python3.5/site-packages/rest_framework/serializers.py", line 488, in to_representation
    attribute = field.get_attribute(instance)
  File "/lib/python3.5/site-packages/rest_framework/fields.py", line 464, in get_attribute
    raise type(exc)(msg)
KeyError: "Got KeyError when attempting to get a value for field `user` on serializer `BulkProductSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `OrderedDict` instance.\nOriginal exception text was: 'fk_user'."

在回溯的 L657 (source here) 我有:

iterable = data.all() if isinstance(data, models.Manager) else data
return [
    self.child.to_representation(item) for item in iterable
]

这让我想知道(在跟踪中进一步挖掘)为什么 serializer.fields 不可用。我怀疑这是因为序列化程序是CustomProductListSerializer 父级,而不是BulkProductSerializer 子级,我是对的。在返回 Response(serializer.data) 之前的 pdb 跟踪中:

(Pdb) serializer.fields
*** AttributeError: 'CustomProductListSerializer' object has no attribute 'fields'
(Pdb) serializer.child.fields
{'uuid': UUIDField(read_only=False, required=False, validators=[]) ...(etc)}
(Pdb) 'user' in serializer.child.fields
True
(Pdb) serializer.data
*** KeyError: "Got KeyError when attempting to get a value for field `user` on serializer `BulkProductSerializer`.\nThe serializer field might be named incorrectly and not match any attribute or key on the `OrderedDict` instance.\nOriginal exception text was: 'fk_user'."
(Pdb) serializer.child.data
{'uuid': '08ec13c0-ab6c-45d4-89ab-400019874c63', ...(etc)}

好的,那么在partial_update 在我的ViewSet 中描述的情况下,获取完整的serializer.data 并在父序列化程序类的响应中返回它的正确方法是什么?

编辑:

class CustomProductListSerializer(serializers.ListSerializer):

    def save(self):
        instances = []
        result = []
        pdb.set_trace()
        for obj in self.validated_data:
            uuid = obj.get('uuid', None)
            if uuid:
                instance = get_object_or_404(Product, uuid=uuid)
                # Specify which fields to update, otherwise save() tries to SQL SET all fields.
                # Gotcha: remove the primary key, because update_fields will throw exception.
                # see https://stackoverflow.com/a/45494046
                update_fields = [k for k,v in obj.items() if k != 'uuid']
                for k, v in obj.items():
                    if k != 'uuid':
                        setattr(instance, k, v)
                instance.save(update_fields=update_fields)
                result.append(instance)
            else:
                instances.append(Product(**obj))

        if len(instances) > 0:
            Product.objects.bulk_create(instances)
            result += instances

        return result

【问题讨论】:

  • 你能发布整个回溯吗?
  • 你也可以发布 CustomProductListSerializer 吗?
  • 那为什么不在你的 ModelSerializer 中使用这个 save 方法呢?为什么要拆分成两个序列化器??
  • 序列化程序的保存方法根据方法路由到CreateUpdate,不适合批量操作。例如,它不能为对象列表正确路由PATCH
  • 您遇到的异常与 ListSerializer 无关。这是关于用户字段“fk_user.username”的来源,我认为这是错误的。因此,模型的代码很有用。我的野客人是你需要一个 SlugRelatedField:django-rest-framework.org/api-guide/relations/#slugrelatedfield

标签: python django django-rest-framework


【解决方案1】:

正如评论中提到的,我仍然认为异常可能是由于 BulkProductSerializer 类中的用户字段,与ListSerializer

没有任何关系

如文档here 中所述,序列化程序 DRF 中可能存在另一个小错误(但很重要)。以下是如何指定list_serializer_class

class CustomListSerializer(serializers.ListSerializer):
    ...

class CustomSerializer(serializers.Serializer):
    ...
    class Meta:
        list_serializer_class = CustomListSerializer

请注意,它是在 Meta 类内部指定的,而不是在外部指定的。所以我认为在你的代码中,它不会理解使用many=True 切换到列表序列化程序。这应该会导致不更新的问题。

更新 - 添加更新嵌套列表序列化程序的示例

似乎问题更多是关于实现嵌套列表序列化程序更新的通用方法,而不是实际错误。因此,我将尝试提供示例代码。

一些注意事项:

  • 如果我们使用 ModelViewSet,列表路由将不允许 PUTPATCH,因此也不允许 update也不会调用 partial_update (reference)。因此我直接使用 POST ,这要简单得多。
  • 如果你想使用PUT/PATCH,那么看这个答案here
  • 我们总是可以在 Post 请求中直接添加一个查询参数,如 allow_updatepartial 来区分 POST/PUT/PATCH
  • 我将使用普通的id,而不是像问题那样使用uuid,它应该非常相似

其实很简单

作为参考,模型如下所示:

class Product(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User, null=True, blank=True)

    def __unicode__(self):
        return self.name

第 1 步:确保将序列化程序更改为 ListSerializer

class ProductViewSet(viewsets.ModelViewSet):
    serializer_class = ProductSerializer
    queryset = Product.objects.all()

    def get_serializer(self, *args, **kwargs):
        # checking for post only so that 'get' won't be affected
        if self.request.method.lower() == 'post':
            data = kwargs.get('data')
            kwargs['many'] = isinstance(data, list)
        return super(ProductViewSet, self).get_serializer(*args, **kwargs)

第 2 步:通过重写 create 函数实现 ListSerializer

class ProductListSerializer(serializers.ListSerializer):
    def create(self, validated_data):
        new_products = [Product(**p) for p in validated_data if not p.get('id')]
        updating_data = {p.get('id'): p for p in validated_data if p.get('id')}
        # query old products
        old_products = Product.objects.filter(id__in=updating_data.keys())
        with transaction.atomic():
            # create new products
            all_products = Product.objects.bulk_create(new_products)
            # update old products
            for p in old_products:
                data = updating_data.get(p.id, {})
                # pop id to remove
                data.pop('id')
                updated_p = Product(id=p.id, **data)
                updated_p.save()
                all_products.append(updated_p)
        return all_products


class ProductSerializer(serializers.ModelSerializer):
    user = serializers.SlugRelatedField(slug_field='username', queryset=User.objects.all())
    id = serializers.IntegerField(required=False)

    class Meta:
        model = Product
        fields = '__all__'
        list_serializer_class = ProductListSerializer

【讨论】:

  • 感谢您的回答。我发布的解决方案实际上是正确的,正如调试器和我提出的事实(覆盖我自己的ListSerializer 类中的数据属性定义。你是对的,文档指定list_serializer_class 应该在@ 987654335@,但对我来说,将它作为类属性(而不是_meta 的属性)的唯一实际结果是在其他端点上的可浏览 API 中呈现 PUT/PATCH 表单(我觉得这很有用,作为 API有能力处理它们)。
  • 不太明白:什么的“实际后果”?我没有看到真正需要您覆盖 DRF 以提供此功能的额外要求。它非常有能力使用与 ListSerializer 交替的 ModelSerializer 进行部分更新。其实你自定义ListSerializer只是为了去掉uuid,其余的和这里django-rest-framework.org/api-guide/serializers/…提供的一样
  • Nathan,很抱歉,您根本不理解这个问题的意义,也不理解调试器的输出。我邀请您尝试使用嵌套关系实现自己的批量 PATCH 端点,以了解为什么 DRF 要求您实现自己的创建/更新逻辑,如您链接到的部分中的文档中所述。
  • 是的,可能不明白问题的重点,不是很清楚。 Anw,对于您在下面的回答,在我在之前评论中提供的链接中,instance 被用作 ListSerializer 中的列表而不是普通实例:book_mapping = {book.id: book for book in instance}。所以我认为这不会是答案中提到的问题。当然,来自 DRF 的变量名“instance”有点误导。我以前用嵌套关系做过 PATCH,我同意按照 DRF 推荐的方式实现它。只是我认为我们不需要覆盖self._data
  • @Escher:我只知道你现在想做的事情。看来解决方案应该仍然比您接受的答案要简单得多,所以我对我的进行了更新。请检查一下。
【解决方案2】:

在我尝试访问 serializer.data 并获取 KeyError 的跟踪点处,我注意到 serializer.data 仅包含来自 initial_data 的键/值对,而不是实例数据(因此,我想, KeyError;某些模型字段的键不存在,因为它是partial_update 请求)。但是,serializer.child.data 确实包含列表中最后一个孩子的所有实例数据。

所以,我转到定义了datarest_framework/serializers.py source

249    @property
250    def data(self):
251        if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
252            msg = (
253                'When a serializer is passed a `data` keyword argument you '
254                'must call `.is_valid()` before attempting to access the '
255                'serialized `.data` representation.\n'
256                'You should either call `.is_valid()` first, '
257                'or access `.initial_data` instead.'
258            )
259            raise AssertionError(msg)
260
261        if not hasattr(self, '_data'):
262            if self.instance is not None and not getattr(self, '_errors', None):
263                self._data = self.to_representation(self.instance)
264            elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
265                self._data = self.to_representation(self.validated_data)
266            else:
267                self._data = self.get_initial()
268        return self._data

第 265 行有问题。我可以通过在断点处调用serializer.child.to_representation({'uuid': '87956604-fbcb-4244-bda3-9e39075d510a', 'product_code': 'foobar'}) 来复制错误。

调用partial_update() 在单个实例上工作正常(因为设置了self.instanceself.to_representation(self.instance) 工作)。但是,对于批量 partial_update() 实现,self.validated_data 缺少模型字段,to_representation() 将不起作用,因此我将无法访问 .data 属性。

一种选择是维护某种 self.instances 的 Product 实例列表,并在第 265 行覆盖 data 的定义:

self._data = self.to_representation(self.instances)

不过,我真的更希望得到在这类问题上更有经验的人的回答,因为我不确定这是否是一个明智的解决方案,因此我将悬赏开放,希望有人能提出更聪明的建议去做。

【讨论】:

    【解决方案3】:

    您的错误与ListSerializer 无关,而是在获取字段user 时出现问题:

    KeyError: "在序列化程序 BulkProductSerializer 上尝试获取字段 user 的值时出现 KeyError。

    序列化器字段可能命名不正确,并且与 OrderedDict 实例上的任何属性或键都不匹配。

    原始异常文本是:'fk_user'。"

    确保您的 Product 模型具有 fk_user 字段。

    您还将BulkProductSerializer 上的user 字段定义为可写但没有告诉序列化程序如何处理它...

    纠正此问题的最简单方法是使用SlugRelatedField

    class BulkProductSerializer(serializers.ModelSerializer):
    
        list_serializer_class = CustomProductListSerializer
    
        user = serializers.SlugRelatedField(
                                slug_field='username',
                                queryset=UserModel.objects.all(),
                                source='fk_user'
        )
    
        class Meta:
            model = Product
            fields = (
                'user',
                'uuid',
                'product_code',
                ...,
            )
    

    这应该可以很好地处理错误,例如当username 不存在时...

    【讨论】:

      【解决方案4】:

      如果您使用 Django 身份验证模型,请删除源代码并设置 read_only=True。

      user = serializers.CharField(read_only=True)

      希望这对你有用

      【讨论】:

        猜你喜欢
        • 2017-10-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-13
        • 1970-01-01
        • 2018-08-15
        • 2017-10-12
        • 1970-01-01
        相关资源
        最近更新 更多