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


74

使用Django REST Framework,标准的ModelSerializer将允许通过将ID作为整数发布来分配或更改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

Answers:


53

最好的解决方案是使用两个不同的字段:一个用于读取,另一个用于写入。如果不做一些繁重的工作,就很难在一个领域中获得想要的东西。

只读字段将是您的嵌套序列化器(ChildSerializer在这种情况下),它将允许您获得与期望的相同的嵌套表示形式。大多数人将其定义为just child,因为到此为止他们已经编写了前端,更改它会引起问题。

只写字段将是PrimaryKeyRelatedField,这是您通常用于基于对象的主键分配对象的字段。这不必是只写的,特别是如果您试图在接收到的内容和发送的内容之间保持对称性时,但这听起来可能最适合您。此字段应具有外键字段source集合(child在此示例中),以便在创建和更新时对其进行正确分配。


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


凯文感谢您的回答。我当时正面临着同样的问题。我在ChildSerializer中添加了两个字段。 parent = ParentSerializer(read_only = True)parent_id = serializers.PrimaryKeyRelatedField(....,write_only = True,....)我也将parentparent_id都添加到ChildSerializer的字段中。但是,我没有在响应中看到任何child_id字段。实际上哪一个是好方便的,但是我想知道是什么原因呢?你有什么主意吗?
hnroot

好答案。只是缺少一些示例代码,就像皮包骨头的答案一样(可能在下面)
分子

70

2020年7月5日更新

这篇文章越来越受到关注,它表明更多的人有类似的情况。因此,我决定添加一种通用方法来处理此问题。如果您有更多的序列化器需要更改为这种格式,则这种通用方法最适合您。

由于DRF没有开箱即用的功能,因此我们需要首先创建一个序列化器字段

from rest_framework import serializers


class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.serializer = kwargs.pop('serializer', None)
        if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
            raise TypeError('"serializer" is not a valid serializer class')

        super().__init__(**kwargs)

    def use_pk_only_optimization(self):
        return False if self.serializer else True

    def to_representation(self, instance):
        if self.serializer:
            return self.serializer(instance, context=self.context).data
        return super().to_representation(instance)

这个类名称给我留下了深刻的印象RelatedFieldAlternative,您可以使用任何您想要的东西。然后,在父级序列化程序中使用此新的序列化程序字段,

class ParentSerializer(ModelSerializer):
   child = RelatedFieldAlternative(queryset=Child.objects.all(), serializer=ChildSerializer)

    class Meta:
        model = Parent
        fields = '__all__'

原始帖子

使用两个不同的领域将是确定(如@Kevin布朗@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


序列化器的完整表示

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屏幕截图


8
几天来我一直在寻找这样的答案。这样的简单性是美丽的。肯定是+1。
Bigbob556677 '18

3
这是一个很好的解决方案,但有一些缺点。drf的模式生成器无法检测到嵌套序列化程序,因此该模式仅将字段显示为PrimaryKeyRelatedField。对于某些项目来说,这可能是可以接受的,但是当您想用redoc或swagger显示您的API模式时,可能会出现问题。因此,即使它不是简单美观,我还是更喜欢两场解决方案。
youngrok

5
我很惊讶DRF需要做这项工作。像这里的大多数人一样,典型的用例是将ForeignKey对象作为数据返回,但将其作为PK接受。我认为这实际上是比创建或仅阅读ForeignKeys更常见的用例...
Kevin Parker

@JPG使用此方法时,在执行补丁更新时出现以下错误。类型不正确。预期pk值,已收到<model>。该字段存在于嵌套序列化程序中。这里出了什么问题?
阿克希尔·马修(AKHIL MATHEW)

可能是,您可能已定义了嵌套的序列化程序或其他内容。我非常确定该给出的示例是一个有效的示例@AKHILMATHEW
JPG

52

如果您想采用该方法并使用2个单独的字段,那么这是Kevin所讨论的内容的一个示例。

在您的模型中。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


4
我收到以下错误消息:Got a TypeError when calling Parent.objects.create(). This may be because you have a writable field on the serializer class that is not a valid argument to Parent.objects.create(). You may need to make the field read-only, or override the ParentSerializer.create() method to handle this correctly.
Gobi Dasu

4

有一种方法可以替代创建/更新操作中的字段:

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

如果您使用的是DRF 3.0,则这是一个很好的解决方案,但要注意的一件事是,创建Parent之后返回的Parent项目将没有嵌套的Child序列化,它将是平坦的(只是主键)。要解决此问题,您还需要覆盖to_representation方法。我在回答重复的问题的答案中添加了这个:stackoverflow.com/questions/26561640/…–
jeffjv

谢谢!我浪费了一天时间试图解决这个问题...所选的答案对我不起作用...
Gutimore

4

这里的一些人放置了一种方法来保留一个字段,但是在检索对象并仅使用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创建带有外键的对象,但是在检索创建的对象时(或在真正的时候)将返回完整的序列化嵌套模型。


1
应该包含在DRF中。:)
Igor Pomaranskiy

2

我认为,凯文(Kevin)概述的方法可能是最好的解决方案,但我永远无法使它起作用。当我同时拥有嵌套的序列化程序和主键字段集时,DRF总是抛出错误。删除一个或另一个将起作用,但显然没有给我我所需的结果。我能想到的最好的办法就是创建两个不同的串行器用于读写,就像这样...

serializers.py:

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.py

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

我遇到了您遇到的同样问题。您是否曾经想过一种使其在一个串行器中起作用的方法? stackoverflow.com/questions/41248271
shanemgrey

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。to_internal_value如果子对象具有有效的ID ,我们将对其进行实例化,以便DRF可以进一步处理该对象。



0

我也陷入同样的​​境地。但是我所做的是,我为以下模型创建了两个序列化器,如下所示:

class Base_Location(models.Model):
    Base_Location_id = models.AutoField(primary_key = True)
    Base_Location_Name = models.CharField(max_length=50, db_column="Base_Location_Name")

class Location(models.Model):
    Location_id = models.AutoField(primary_key = True)
    Location_Name = models.CharField(max_length=50, db_column="Location_Name")
    Base_Location_id = models.ForeignKey(Base_Location, db_column="Base_Location_id", related_name="Location_Base_Location", on_delete=models.CASCADE)

这是我父母的序列化器

class BaseLocationSerializer(serializers.ModelSerializer):
    class Meta:
        model = Base_Location
        fields = "__all__"

我仅将此序列化程序用于获取请求,因此作为响应,我也由于嵌套序列化程序而获得了带有外键的数据

class LocationSerializerList(serializers.ModelSerializer): <-- using for get request 
    Base_Location_id = BaseLocationSerializer() 

    class Meta:
        model = Location
        fields = "__all__"

邮递员中get方法请求和响应的屏幕截图

我仅将此序列化程序用于发布请求,因此在发送发布请求时,我不需要包括任何其他信息,而不必包含主键字段值

class LocationSerializerInsert(serializers.ModelSerializer): <-- using for post request
    class Meta:
        model = Location
        fields = "__all__"

邮递员中发帖方法请求和响应的屏幕截图


0

根据JPGBono的回答,我提出了一个解决方案,该解决方案也可以处理DRF的OpenAPI Schema生成器。

实际的字段类是:

from rest_framework import serializers


class ModelRepresentationPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.response_serializer_class = kwargs.pop('response_serializer_class', None)
        if self.response_serializer_class is not None \
                and not issubclass(self.response_serializer_class, serializers.Serializer):
            raise TypeError('"serializer" is not a valid serializer class')

        super(ModelRepresentationPrimaryKeyRelatedField, self).__init__(**kwargs)

    def use_pk_only_optimization(self):
        return False if self.response_serializer_class else True

    def to_representation(self, instance):
        if self.response_serializer_class is not None:
            return self.response_serializer_class(instance, context=self.context).data
        return super(ModelRepresentationPrimaryKeyRelatedField, self).to_representation(instance)

扩展的AutoSchema类是:

import inspect
from rest_framework.schemas.openapi import AutoSchema

from .fields import ModelRepresentationPrimaryKeyRelatedField


class CustomSchema(AutoSchema):
    def _map_field(self, field):
        if isinstance(field, ModelRepresentationPrimaryKeyRelatedField) \
                and hasattr(field, 'response_serializer_class'):
            frame = inspect.currentframe().f_back
            while frame is not None:
                method_name = frame.f_code.co_name
                if method_name == '_get_request_body':
                    break
                elif method_name == '_get_responses':
                    field = field.response_serializer_class()
                    return super(CustomSchema, self)._map_field(field)

                frame = frame.f_back

        return super(CustomSchema, self)._map_field(field)

然后在Dganjo的项目设置中,您可以定义要在全局范围内使用的新Schema类,例如:

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': '<path_to_custom_schema>.CustomSchema',
}

最后,您可以在模型中使用新的字段类型,例如:

class ExampleSerializer(serializers.ModelSerializer):
    test_field = ModelRepresentationPrimaryKeyRelatedField(queryset=Test.objects.all(), response_serializer_class=TestListSerializer)

0

在找到答案之前,我首先实现了与JPG解决方案类似的方法,并注意到它破坏了内置Django Rest Framework的模板。现在,这没什么大不了的(因为他们的解决方案可以通过request / postman / AJAX / curl / etc很好地工作),但是如果有人(例如我)是新来的,并且希望内置的DRF表单可以帮助他们方式,这是我的解决方案(在清理并整合了JPG的一些想法之后):

class NestedKeyField(serializers.PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.serializer = kwargs.pop('serializer', None)
        if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
            raise TypeError('You need to pass a instance of serialzers.Serializer or atleast something that inherits from it.')
        super().__init__(**kwargs)

    def use_pk_only_optimization(self):
        return not self.serializer

    def to_representation(self, value):
        if self.serializer:
            return dict(self.serializer(value, context=self.context).data)
        else:
            return super().to_representation(value)

    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        return OrderedDict([
            (
                self.to_representation(item)['id'] if self.serializer else self.to_representation(item), # If you end up using another column-name for your primary key, you'll have to change this extraction-key here so it maps the select-element properly.
                self.display_value(item)
            )
            for item in queryset
        ])

下面是子序列化器类的示例:

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = ChildModel
        fields = '__all__'

父级序列化程序类:

class ParentSerializer(serializers.ModelSerializer):
    same_field_name_as_model_foreign_key = NestedKeyField(queryset=ChildModel.objects.all(), serializer=ChildSerializer)
    class Meta:
        model = ParentModel
        fields = '__all__'

0

这就是我正在使用的所有内容。这可能是最简单,最直接的方法,不需要任何黑客手段,并且可以直接使用DRF,而不会遇到麻烦。很高兴听到对这种方法的分歧。

在视图的perform_create(或等效视图)中,获取与POST请求中发送的字段相对应的FK模型数据库对象,然后将发送到序列化器中。POST请求中的字段可以是可用于过滤和定位数据库对象的任何内容,而不必是ID。

在此处进行记录:https : //www.django-rest-framework.org/api-guide/generic-views/#genericapiview

这些挂钩对于设置隐式存在于请求中但不属于请求数据的属性特别有用。例如,您可以基于请求用户或基于URL关键字参数在对象上设置属性。

def perform_create(self,serializer):serializer.save(user = self.request.user)

该方法还具有以下优点:通过在对GET或POST的响应中不为子级发送嵌套表示,可以保持读写侧的奇偶性。

给定OP发布的示例:

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):
    # Note this is different from the OP's example. This will send the
    # child name in the response
    child = serializers.ReadOnlyField(source='child.name')

    class Meta:
        model = Parent
        fields = ('name', 'phone_number', 'child')

在视图的perform_create中:

class SomethingView(generics.ListCreateAPIView):
    serializer_class = ParentSerializer
    
    def perform_create(self, serializer):
        child_name = self.request.data.get('child_name', None)
        child_obj = get_object_or_404(Child.objects, name=child_name)
        serializer.save(child=child_obj)

PS:请注意,我并未在上面的代码段中对此进行测试,但是它基于我在许多地方使用的模式,因此它应该可以正常工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.