Django REST Framework从超链接标识字段

时间:2015-09-13 03:02:14

标签: python django rest django-rest-framework

使用validated_data时,我很难从HyperlinkedIdentityField获取网址(或任何类型的ID)。我需要URL,因为我正在进行可写的嵌套表示,我想知道嵌套表示是指现有对象(指定的url)还是新对象(未指定url)。我已阅读有关自定义ListSerializer更新的文档中的example,但这似乎依赖于客户端发送不存在的id,这对我来说不起作用,因为我想要请改用URL。我已检查validated_data是否有HyperlinkedIdentityField的顶级(非嵌套)对象,并且ID也未在此处公开。

编辑:我现在知道这是因为HyperlinkedIdentityField设置了read_only属性,并且在生成验证数据时会跳过只读字段。欢迎提示如何解决这个问题。

此电话簿应用中有两种型号:EntryPhoneEntry有多个Phone个。如果这是相关的,模型代码如下:

from django.db import models


class Entry(models.Model):
    name = models.CharField(max_length=200)


class Phone(models.Model):
    type = models.CharField(max_length=20)
    number = models.CharField(max_length=50)
    parent = models.ForeignKey(Entry, related_name='phones')

我的序列化定义如下:

class PhoneListSerializer(serializers.ListSerializer):
    def update(self, instance, validated_data): 
        print repr(validated_data)
        url_of = lambda p : self.child.to_representation(p)['url']
        existing_instances = { url_of(p): p for p in instance }
        existing_submitted_instances = { item['url']: item for item in validated_data if 'url' in item }
        new_submitted_instances = [ item for item in validated_data if 'url' not in item ]
        urls_to_delete = existing_instances.viewkeys() - existing_submitted_instances.viewkeys()                                                       
        objects_to_delete = [existing_instances[u] for u in urls_to_delete if u in existing_instances]                                                 
        objects_to_update = [(existing_instances[u], p) for u, p in six.iteritems(existing_submitted_instances) if u in existing_instances]

        result = []
        for o in objects_to_delete:
            o.delete()
        for existing, data in objects_to_update:                          
            result.append(self.child.update(existing, data))
        for data in new_submitted_instances:                              
            data['parent'] = self.root.instance
            result.append(self.child.create(data))
        return result

class PhoneSerializer(serializers.HyperlinkedModelSerializer):
    url = serializers.HyperlinkedIdentityField(
        view_name='phone', lookup_field='id')
    class Meta:
        model = Phone
        list_serializer_class = PhoneListSerializer
        fields = ('type', 'number', 'url')

class EntrySerializer(serializers.HyperlinkedModelSerializer):
    url = serializers.HyperlinkedIdentityField(
        view_name='entry', lookup_field='id')
    phones = PhoneSerializer(many=True, required=False)

    class Meta:
        model = Entry
        fields = ('url', 'name', 'phones')

    def update(self, instance, validated_data):
        print repr(validated_data)
        # pop this first so super does not complain about writable nested
        # serializers. we will update phones ourselves.
        phone_data = validated_data.pop('phones', [])
        phones_field = self.fields['phones']

        instance = super(EntrySerializer, self).update(instance, validated_data)

        phones_field.update(instance.phones.all(), phone_data)

        return instance

    def create(self, validated_data):
        phone_data = validated_data.pop('phones', [])
        new_entry = Entry.objects.create(**validated_data)
        # TODO atomically do this
        for phone_validated_data in phone_data:
            Phone.objects.create(parent=new_entry, **phone_validated_data)
        return new_entry

目前,数据输出有效,但重新提交数据(即使使用URL)会导致所有条目的电话被删除,然后使用新ID重新创建。

编辑:尝试使用HyperlinkedRelatedField(以及ModelSerializer代替HyperlinkedModelSerializer)得到更多(该字段成功将对象拉出) ,但在序列化程序调用{​​{1}}

时仍然失败

代码:

fields.set_value

此操作失败,因为它最终调用class PhoneSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedRelatedField( required=False, view_name='phone', lookup_field='id', queryset=Phone.objects, source='*') ,并且它将调用rest_framework.fields.set_value(validated_data, [], <Phone object>),其失败并显示'Phone object is is iterable'。

validated_data.update(<Phone object>)

其他尝试:

Traceback (most recent call last): [...snip...] File "/app/phonebook_be/views.py", line 41, in entry if serializer.is_valid(raise_exception=True): File "/venv/lib/python2.7/site-packages/rest_framework/serializers.py", line 191, in is_valid self._validated_data = self.run_validation(self.initial_data) File "/venv/lib/python2.7/site-packages/rest_framework/serializers.py", line 371, in run_validation value = self.to_internal_value(data) File "/venv/lib/python2.7/site-packages/rest_framework/serializers.py", line 404, in to_internal_value validated_value = field.run_validation(primitive_value) File "/venv/lib/python2.7/site-packages/rest_framework/serializers.py", line 522, in run_validation value = self.to_internal_value(data) File "/venv/lib/python2.7/site-packages/rest_framework/serializers.py", line 552, in to_internal_value validated = self.child.run_validation(item) File "/venv/lib/python2.7/site-packages/rest_framework/serializers.py", line 371, in run_validation value = self.to_internal_value(data) File "/venv/lib/python2.7/site-packages/rest_framework/serializers.py", line 414, in to_internal_value set_value(ret, field.source_attrs, validated_value) File "/venv/lib/python2.7/site-packages/rest_framework/fields.py", line 96, in set_value dictionary.update(value) File "/venv/lib/python2.7/_abcoll.py", line 568, in update for key, value in other: TypeError: 'Phone' object is not iterable :渲染时导致错误

source='id'

2 个答案:

答案 0 :(得分:1)

您目前正在手动比较网址。然而,这有一些问题:

url_of = lambda p : self.child.to_representation(p)['url']
existing_instances = { url_of(p): p for p in instance }

第二行很昂贵,因为instance是一个查询集,因此只要你有大量数据,就可以成为内存耗尽,因为你将为匹配queryset的所有实例创建一个dict。 / p>

DRF本身支持解析URL以获取身份对象,而不是这样做:

class PhoneSerializer(serializers.ModelSerializer):
     url = serializers.HyperlinkedRelatedField(
         required=False,
         view_name='phone',
         lookup_field='id',
     )

请注意HyperlinkedRelatedField的使用。与HyperlinkedIdentityField(子类HyperlinkedRelatedField)不同,HyperlinkedRelatedField不是只读的。它能够通过反转URL然后使用url中的kwargs在db中查找对象来解析URL并从URL本身获取模型。因此,您可以依赖该行为来查找db中的现有对象。请注意,该字段是可选的,允许API的客户端省略它,在这种情况下,URL将不会被解析,因此可以创建新对象。

class PhoneListSerializer(serializers.ListSerializer):
    def update(self, instance, validated_data): 
        to_update = filter(lambda i: i.get('url'), validated_data)
        to_create = filter(lambda i: not i.get('url'), validated_data)
        data = []
        for i in to_update:
            data.append(self.child.update(i['url'], i))
        for i in to_create:
            data.append(self.child.create(i))
        return data

希望这会有所帮助。请注意,我是从记忆中做到的,因此可能需要注意一些事项,但这个概念通常应该有用。

最后有一些关于HyperlinkedRelatedField - http://www.django-rest-framework.org/api-guide/relations/#hyperlinkedrelatedfield

的文档

答案 1 :(得分:1)

因此,为了解决这个问题,我在HiddenField上创建了一个只写字段(受PhoneSerializer启发),该字段从提交的'url'值中获取其值:

class WriteOnlySynonymField(serializers.Field):

    def __init__(self, **kwargs):
        kwargs['default'] = serializers.empty
        kwargs['required'] = False
        kwargs['write_only'] = True
        self.synonym_for = kwargs.pop('synonym_for')
        super(WriteOnlySynonymField, self).__init__(**kwargs)

    def get_value(self, dictionary):
        return dictionary.get(self.synonym_for, serializers.empty)

    def to_internal_value(self, data):
        return data


class PhoneSerializer(serializers.ModelSerializer):
    url = MultiKeyHyperlinkedIdentityField(
        view_name='phone',
        lookup_kwarg_to_field={'id': 'id', 'entry_id': 'parent_id'})
    submitted_url = WriteOnlySynonymField(synonym_for='url')

    class Meta:
        model = Phone
        list_serializer_class = PhoneListSerializer
        fields = ('type', 'number', 'url', 'submitted_url')

列表序列化程序更新的相关位稍有变化,以使用新字段名称:

existing_submitted_instances = {item['submitted_url']: item
                                for item in validated_data
                                if 'submitted_url' in item}
new_submitted_instances = [item for item in validated_data
                           if 'submitted_url' not in item]