Django Rest Framework - 读取嵌套数据,写入整数

32 投票
5 回答
6566 浏览
提问于 2025-04-29 14:41

到目前为止,我对Django Rest Framework非常满意,这也是我几乎不敢相信代码中会有这么大的遗漏。希望有人知道怎么解决这个问题:

class PinSerializer(serializers.ModelSerializer):
    item = ItemSerializer(read_only=True, source='item')
    item = serializers.IntegerSerializer(write_only=True)

    class Meta:
        model = Pin

目标是

The goal here is to read:
{pin: item: {name: 'a', url: 'b'}}
but to write using an id
{pin: item: 10}

另一种方法是使用两个序列化器,但这看起来真的是个很糟糕的解决方案:django rest framework模型序列化器 - 读取嵌套,写入扁平

暂无标签

5 个回答

0

我对Wezen42的回答进行了重构。所以自定义字段类看起来会是这样的:

from rest_framework import serializers


class RepresentativePkRelatedField(serializers.PrimaryKeyRelatedField):
    def __init__(self, serializer_class, **kwargs):
        self.serializer_class = serializer_class
        super().__init__(**kwargs)

    def to_representation(self, value):
        if self.pk_field is not None:
            return self.pk_field.to_representation(value.pk)

        pk = value.pk
        queryset = self.get_queryset().filter(pk=pk)
        if queryset.exists():
            return self.serializer_class(queryset.first()).data

要使用这个,您需要在您的序列化器中包含类似的代码:

profile = RepresentativePkRelatedField(
        queryset=Item.objects.all(),
        serializer_class=ItemSerializer,
    )
1

我创建了一个字段类型,目的是解决在保存数据时,外键用整数表示的问题,以及在读取数据时需要处理嵌套数据的请求。

这是这个类的代码:

class NestedRelatedField(serializers.PrimaryKeyRelatedField):
"""
    Model identical to PrimaryKeyRelatedField but its
    representation will be nested and its input will
    be a primary key.
"""

def __init__(self, **kwargs):
    self.pk_field = kwargs.pop('pk_field', None)
    self.model = kwargs.pop('model', None)
    self.serializer_class = kwargs.pop('serializer_class', None)
    super().__init__(**kwargs)

def to_representation(self, data):
    pk = super(NestedRelatedField, self).to_representation(data)
    try:
        return self.serializer_class(self.model.objects.get(pk=pk)).data
    except self.model.DoesNotExist:
        return None

def to_internal_value(self, data):
    return serializers.PrimaryKeyRelatedField.to_internal_value(self, data)

接下来是如何使用这个类的示例:

class PostModelSerializer(serializers.ModelSerializer):

    message = NestedRelatedField(
         queryset=MessagePrefix.objects.all(),
         model=MessagePrefix,
         serializer_class=MessagePrefixModelSerializer
   )

希望这对你有帮助。

2

你可以创建一个自定义的序列化字段(http://www.django-rest-framework.org/api-guide/fields

这个例子来自上面的链接:

class ColourField(serializers.WritableField):
    """
    Color objects are serialized into "rgb(#, #, #)" notation.
    """
    def to_native(self, obj):
        return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)

    def from_native(self, data):
        data = data.strip('rgb(').rstrip(')')
        red, green, blue = [int(col) for col in data.split(',')]
        return Color(red, green, blue)

然后在你的序列化器类中使用这个字段。

16

如果你在使用DRF 3.0,可以实现一个新的 to_internal_value 方法,来修改项目字段,把它改成一个主键关联字段,这样就能支持简单的写入操作。这个 to_internal_value 方法接收未经验证的输入数据,然后返回经过验证的数据,这些数据会在 serializer.validated_data 中使用。具体可以查看文档:http://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data

所以在你的情况下,可以这样写:

class ItemSerializer(ModelSerializer):
    class Meta:
        model = Item

class PinSerializer(ModelSerializer):
    item = ItemSerializer() 

    # override the nested item field to PrimareKeyRelatedField on writes
    def to_internal_value(self, data):
         self.fields['item'] = serializers.PrimaryKeyRelatedField(queryset=Item.objects.all())
         return super(PinSerializer, self).to_internal_value(data)

    class Meta:
        model = Pin

有两点需要注意:可浏览的网页API仍然会认为写入操作是嵌套的。我不太确定怎么解决这个问题,不过我只是用网页界面来调试,所以也不是太大的问题。另外,在你写入数据后,返回的项目将是简单的项目,而不是嵌套的。要解决这个问题,你可以添加一些代码,强制读取时总是使用项目序列化器。

def to_representation(self, obj):
    self.fields['item'] = ItemSerializer()
    return super(PinSerializer, self).to_representation(obj)

这个想法是从Anton Dmitrievsky的回答中得到的,具体可以参考这里:DRF: Simple foreign key assignment with nested serializers?

31

Django 让你通过 item 属性来访问你 Pin 上的 Item,但实际上它是把这个关系存储为 item_id。你可以在序列化器中使用这种方法,来解决一个问题:在 Python 对象中,不能有两个同名的属性(这在你的代码中会遇到麻烦)。

最好的做法是使用 PrimaryKeyRelatedField,并加上 source 参数。这样可以确保在字段验证时,正确地将 "item_id": <id> 转换为 "item": <instance>(这发生在序列化器的 validate 调用之前)。这样,你就可以在 validatecreateupdate 方法中操作完整的对象。你的最终代码应该是:

class PinSerializer(serializers.ModelSerializer):
    item = ItemSerializer(read_only=True)
    item_id = serializers.PrimaryKeyRelatedField(write_only=True,
                                                 source='item',
                                                 queryset=Item.objects.all())

    class Meta:
        model = Pin
        fields = ('id', 'item', 'item_id',)

注意1:我还去掉了读取字段上的 source='item',因为那是多余的。

注意2:我觉得 Django Rest 的设置有点不直观,因为如果 Pin 的序列化器没有指定 Item 的序列化器,它返回的 item_id 是 "item": <id> 而不是 "item_id": <id>,但这不是重点。

这种方法甚至可以用于正向和反向的“多”关系。例如,你可以使用一个 pin_ids 的数组来设置一个 Item 上的所有 Pins,代码如下:

class ItemSerializer(serializers.ModelSerializer):
    pins = PinSerializer(many=True, read_only=True)
    pin_ids = serializers.PrimaryKeyRelatedField(many=True,
                                                 write_only=True,
                                                 source='pins',
                                                 queryset=Pin.objects.all())

    class Meta:
        model = Item
        fields = ('id', 'pins', 'pin_ids',)

我之前推荐的另一种策略是使用 IntegerField 直接设置 item_id。假设你使用的是 OneToOneField 或 ForeignKey 来将你的 Pin 和 Item 关联起来,你可以直接将 item_id 设置为一个整数,而完全不使用 item 字段。这会削弱验证,可能导致数据库层面出现约束错误。如果你想跳过验证的数据库调用,或者在你的 validate/create/update 代码中需要 ID 而不是对象,或者需要同时可写的同源字段,这可能更好,但我不再推荐这种做法。完整的代码行是:

item_id = serializers.IntegerField(write_only=True)

撰写回答