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

DRF: Simple foreign key assignment with nested serializers?

使用 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

这里最好的解决办法是使用两个不同的领域:一个用于阅读,另一个用于写作。如果不做一些繁重提升,很难在一个字段.

中得到你想要的东西

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

只写字段将 PrimaryKeyRelatedField, which is what you would typically use for assigning objects based on their primary key. This does not have to be write-only, especially if you are trying to go for symmetry between what is received and what is sent, but it sounds like that might suit you best. This field should have a source 设置为外键字段(本例中为 child),因此它会在创建和更新时正确分配它。


这个问题已经在讨论组里提过几次了,我觉得还是最好的方案。感谢 Sven Maurer for pointing it out.

我认为 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

如果您想采用这种方法并使用 2 个单独的字段,这里有一个 Kevin 的回答所谈论内容的示例。

在你的 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=child 可以让 child_id 像默认情况下的子项一样,如果它没有被覆盖(我们想要的行为)。 write_only=True 使 child_id 可写入,但由于 id 已显示在 ChildSerializer.

中,因此不会在响应中显示它

这是我解决这个问题的方法。

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 可以进一步处理该对象。

有一种方法可以在 create/update 操作中替换字段:

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

2020 年 7 月 5 日更新

这个post受到越来越多的关注,说明有类似情况的人越来越多。所以我决定添加一个通用方式来处理这个问题。如果您有更多需要更改为这种格式的序列化器,这种通用方式最适合您

由于 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)

我对这个class名字印象不是很好,RelatedFieldAlternative,你可以随便用。 然后在你的父序列化器中使用这个新的序列化器字段作为,

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

    class Meta:
        model = Parent
        fields = '__all__'

原版Post

使用两个不同的字段 ok(如 @Kevin Brown and @joslarson 所述),但我认为这不是 完美(对我来说)。因为从一个键(child)获取数据并向另一个键(child_id)发送数据对于前端[=76=来说可能有点模棱两可] 开发人员。 (完全没有冒犯)


所以,我在这里建议的是,重写 ParentSerializerto_representation() 方法就可以了。

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__'

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



Advantage of this method?

通过使用这种方法,我们不需要创建和读取两个单独的字段。这里创建和读取都可以使用child键来完成。


创建 parent 实例的示例负载

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



截图

这里的一些人提出了一种方法来保留一个字段,但在检索对象时仍然能够获取详细信息并仅使用 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)

然后 class 本身:

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,但 return 在检索您创建的对象(或任何时候)时将是完整的序列化嵌套模型。

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

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

我也遇到过同样的情况。但是我所做的是,我为以下模型创建了两个序列化程序,如下所示:

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)

This is my parent serializer

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

I'm using this serializer only for get request so in response i got data with foreign key also because of nested serializer

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

    class Meta:
        model = Location
        fields = "__all__"

Screenshot of get method request and response in postman

I'm using this serializer only for post request so while sending post request i do not need to include any additional information rather than primary key field value

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

Screenshot of post method request and response in postman

根据 and 两位的回答,我想出了一个解决方案,它也可以处理 DRF 的 OpenAPI Schema 生成器。

实际字段class是:

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 class 是:

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 项目设置中,您可以定义这个新架构 class 以在全球范围内使用,例如:

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)

在找到这个答案之前,我首先实现了类似于 的东西,并注意到它破坏了 built-in Django Rest Framework 的模板。现在,这没什么大不了的(因为他们的解决方案通过 requests/postman/AJAX/curl/etc 非常有效),但是如果有人是新手(像我一样)并希望 built-in 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:

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

父序列化器Class:

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

这就是我一直在使用的东西。这可能是最简单、最直接的方法,不需要 hack 等,并且直接使用 DRF 而无需绕圈子。很高兴听到对这种方法的不同意见。

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

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

These hooks are particularly useful for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.

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:请注意,我没有测试上面的代码片段,但是它基于我在很多地方使用的模式,所以它应该按原样工作。