DRF:使用嵌套序列化程序进行简单的外键分配?

问题描述 投票:46回答:8

使用Django REST Framework,标准的ModelSerializer将允许通过将ID作为整数进行POST来分配或更改ForeignKey模型关系。

从嵌套的序列化器中获取此行为的最简单方法是什么?

注意,我只讨论分配现有数据库对象,而不是嵌套创建。

我曾经在序列化程序中使用额外的'id'字段以及自定义的createupdate方法来破解这个问题,但这对我来说是一个看似简单且常见的问题,我很想知道最好的方法。

class Child(models.Model):
    name = CharField(max_length=20)

class Parent(models.Model):
    name = CharField(max_length=20)
    phone_number = models.ForeignKey(PhoneNumber)
    child = models.ForeignKey(Child)

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child

class ParentSerializer(ModelSerializer):
    # phone_number relation is automatic and will accept ID integers
    children = ChildSerializer() # this one will not

    class Meta:
        model = Parent
python django django-rest-framework
8个回答
37
投票

这里最好的解决方案是使用两个不同的字段:一个用于阅读,另一个用于写入。如果不做一些繁重的工作,很难在一个领域找到你想要的东西。

只读字段将是您的嵌套序列化程序(在本例中为ChildSerializer),它将允许您获得您期望的相同嵌套表示。大多数人将此定义为child,因为他们已经通过这一点编写了前端并且更改它会导致问题。

只写字段将是PrimaryKeyRelatedField,这是您通常用于根据主键分配对象的内容。这不一定是只写的,特别是如果你试图在接收的内容和发送的内容之间保持对称,但听起来这可能最适合你。此字段应将a source设置为外键字段(在此示例中为child),以便在创建和更新时正确分配它。


这已经在讨论组中提出过几次了,我认为这仍然是最好的解决方案。感谢Sven Maurer for pointing it out


35
投票

以下是Kevin的答案所讨论的一个例子,如果你想采用这种方法并使用2个单独的字段。

在你的models.py中......

class Child(models.Model):
    name = CharField(max_length=20)

class Parent(models.Model):
    name = CharField(max_length=20)
    phone_number = models.ForeignKey(PhoneNumber)
    child = models.ForeignKey(Child)

然后serializers.py ...

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child

class ParentSerializer(ModelSerializer):
    # if child is required
    child = ChildSerializer(read_only=True) 
    # if child is a required field and you want write to child properties through parent
    # child = ChildSerializer(required=False)
    # otherwise the following should work (untested)
    # child = ChildSerializer() 

    child_id = serializers.PrimaryKeyRelatedField(
        queryset=Child.objects.all(), source='child', write_only=True)

    class Meta:
        model = Parent

设置source=childchild_id作为孩子将默认为没有被覆盖(我们期望的行为)。 write_only=True使child_id可以写入,但由于id已经出现在ChildSerializer中,因此不会出现在响应中。


27
投票

使用两个不同的字段就可以了(如@Kevin Brown@joslarson提到的那样),但我认为它并不完美(对我来说)。因为从一个密钥(child)获取数据并将数据发送到另一个密钥(child_id)可能对前端开发人员来说有点模棱两可。 (完全没有冒犯) 所以,我在这里建议的是,覆盖to_representation()ParentSerializer方法将完成这项工作。

def to_representation(self, instance):
    response = super().to_representation(instance)
    response['child'] = ChildSerializer(instance.child).data
    return response


完整的Serializer表示

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child
        fields = '__all__'


class ParentSerializer(ModelSerializer):
    class Meta:
        model = Parent
        fields = '__all__'

    def to_representation(self, instance):
        response = super().to_representation(instance)
        response['child'] = ChildSerializer(instance.child).data
        return response

这种方法的优点?

通过使用此方法,我们不需要两个单独的字段来创建和读取。在这里,创建和阅读都可以使用child键完成。 示例有效负载以创建parent实例

{
        "name": "TestPOSTMAN_name",
        "phone_number": 1,
        "child": 1
    }

截图 POSTMAN screenshot


3
投票

有一种方法可以在创建/更新操作上替换字段:

class ChildSerializer(ModelSerializer):
    class Meta:
        model = Child

class ParentSerializer(ModelSerializer):
    child = ChildSerializer() 

    # called on create/update operations
    def to_internal_value(self, data):
         self.fields['child'] = serializers.PrimaryKeyRelatedField(
             queryset=Child.objects.all())
         return super(ParentSerializer, self).to_internal_value(data)

    class Meta:
        model = Parent

2
投票

我认为Kevin概述的方法可能是最好的解决方案,但我无法让它发挥作用。当我同时拥有嵌套的序列化程序和主键字段集时,DRF不断抛出错误。删除一个或另一个将起作用,但显然没有给我我需要的结果。我能想到的最好的是创建两个不同的读取和写入序列化器,就像这样......

serialize认识.朋友:

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = Child

class ParentSerializer(serializers.ModelSerializer):
    class Meta:
        abstract = True
        model = Parent
        fields = ('id', 'child', 'foo', 'bar', 'etc')

class ParentReadSerializer(ParentSerializer):
    child = ChildSerializer()

views.朋友

class ParentViewSet(viewsets.ModelViewSet):
    serializer_class = ParentSerializer
    queryset = Parent.objects.all()
    def get_serializer_class(self):
        if self.request.method == 'GET':
            return ParentReadSerializer
        else:
            return self.serializer_class

2
投票

这是我如何解决这个问题。

serializers.py

class ChildSerializer(ModelSerializer):

  def to_internal_value(self, data):
      if data.get('id'):
          return get_object_or_404(Child.objects.all(), pk=data.get('id'))
      return super(ChildSerializer, self).to_internal_value(data)

你只需要传递你的嵌套子序列化器,就像你从序列化器中获取它一样,即作为json / dictionary的child。在to_internal_value中,如果子对象具有有效ID,则我们实例化该子对象,以便DRF可以进一步使用该对象。


2
投票

这里的一些人已经设置了一种方法来保留一个字段,但仍然能够在检索对象时获取细节并仅使用ID创建它。如果人们感兴趣,我做了一个更通用的实现:

首先是测试:

from rest_framework.relations import PrimaryKeyRelatedField

from django.test import TestCase
from .serializers import ModelRepresentationPrimaryKeyRelatedField, ProductSerializer
from .factories import SomethingElseFactory
from .models import SomethingElse


class TestModelRepresentationPrimaryKeyRelatedField(TestCase):
    def setUp(self):
        self.serializer = ModelRepresentationPrimaryKeyRelatedField(
            model_serializer_class=SomethingElseSerializer,
            queryset=SomethingElse.objects.all(),
        )

    def test_inherits_from_primary_key_related_field(self):
        assert issubclass(ModelRepresentationPrimaryKeyRelatedField, PrimaryKeyRelatedField)

    def test_use_pk_only_optimization_returns_false(self):
        self.assertFalse(self.serializer.use_pk_only_optimization())

    def test_to_representation_returns_serialized_object(self):
        obj = SomethingElseFactory()

        ret = self.serializer.to_representation(obj)

        self.assertEqual(ret, SomethingElseSerializer(instance=obj).data)

然后是班级本身:

from rest_framework.relations import PrimaryKeyRelatedField

class ModelRepresentationPrimaryKeyRelatedField(PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.model_serializer_class = kwargs.pop('model_serializer_class')
        super().__init__(**kwargs)

    def use_pk_only_optimization(self):
        return False

    def to_representation(self, value):
        return self.model_serializer_class(instance=value).data

如果你在某个地方有一个序列化器,用法是这样的:

class YourSerializer(ModelSerializer):
    something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)

这将允许您仅使用PK创建具有外键的对象,但在检索您创建的对象时(或者无论何时真实地)将返回完整的序列化嵌套模型。


0
投票

有一个包!查看Drf Extra Fields包中的PresentablePrimaryKeyRelatedField。

https://github.com/Hipo/drf-extra-fields

© www.soinside.com 2019 - 2024. All rights reserved.