Django Rest Framework - 读取嵌套数据,写入整数
到目前为止,我对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 个回答
我对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,
)
我创建了一个字段类型,目的是解决在保存数据时,外键用整数表示的问题,以及在读取数据时需要处理嵌套数据的请求。
这是这个类的代码:
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
)
希望这对你有帮助。
你可以创建一个自定义的序列化字段(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)
然后在你的序列化器类中使用这个字段。
如果你在使用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?
Django 让你通过 item
属性来访问你 Pin 上的 Item,但实际上它是把这个关系存储为 item_id
。你可以在序列化器中使用这种方法,来解决一个问题:在 Python 对象中,不能有两个同名的属性(这在你的代码中会遇到麻烦)。
最好的做法是使用 PrimaryKeyRelatedField
,并加上 source
参数。这样可以确保在字段验证时,正确地将 "item_id": <id>
转换为 "item": <instance>
(这发生在序列化器的 validate
调用之前)。这样,你就可以在 validate
、create
和 update
方法中操作完整的对象。你的最终代码应该是:
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)