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

Posted

技术标签:

【中文标题】DRF:使用嵌套序列化程序的简单外键分配?【英文标题】:DRF: Simple foreign key assignment with nested serializers? 【发布时间】:2015-07-09 03:58:03 【问题描述】:

使用 Django REST 框架,标准 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

【问题讨论】:

【参考方案1】:

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

在视图的perform_create(或等效)中,获取与POST请求中发送的字段对应的FK模型数据库对象,然后将that发送到Serializer中。 POST 请求中的字段可以是任何可用于过滤和定位 DB 对象的字段,不必是 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')

在 View 的 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:请注意,我没有在 sn-p 上面测试过这个,但是它基于我在很多地方使用的模式,所以它应该可以正常工作。

【讨论】:

【参考方案2】:

在我找到这个答案之前,我首先实现了类似于JPG's solution 的东西,并注意到它破坏了内置的 Django Rest Framework 的模板。现在,这没什么大不了的(因为他们的解决方案通过请求/邮递员/AJAX/curl/等非常有效),但是如果有人是新人(比如我)并且想要内置的 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__'

【讨论】:

【参考方案3】:

根据JPG 和Bono 的回答,我想出了一个处理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)

【讨论】:

此方法 (_map_field) 已被弃用。现在叫map_field【参考方案4】:

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

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

Screenshot of get method request and response in postman

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

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

Screenshot of post method request and response in postman

【讨论】:

【参考方案5】:

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

原帖

使用两个不同的字段会ok(正如@Kevin Brown 和@joslarson 提到的),但我认为它不是完美强>(对我来说)。因为从一个键 (child) 获取数据并将数据发送到另一个键 (child_id) 对于 前端 开发人员来说可能有点模棱两可。 (完全没有冒犯) 所以,我在这里的建议是,重写 ParentSerializerto_representation() 方法就可以了。

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
    

截图

【讨论】:

我这几天一直在寻找这样的答案。这种简单是美丽的。肯定 +1。 这是一个优雅的解决方案,但有一些缺点。 drf 的模式生成器无法检测嵌套序列化程序,因此模式只会将该字段显示为 PrimaryKeyRelatedField。对于某些项目来说这可能是可以接受的,但是当你想用 redoc 或 swagger 来展示你的 API 模式时,这可能是个问题。所以我更喜欢双场解决方案,即使它并不简单和美观。 我很惊讶 DRF 需要这项工作才能做到这一点。像这里的大多数人一样,典型的用例是将 ForeignKey 对象作为数据返回,但将它们作为 PK 接受。我认为这实际上是一个比创建或专门读取 ForeignKeys 更常见的用例...... 可能你被定义了一个nested serializer 什么的。我非常确定这个给定的例子是一个有效的例子@AKHILMATHEW 您的通用方法看起来对我很有吸引力,但我总是得到unhashable type: 'ReturnDict' - to_representation 方法工作正常 - 想知道可能是什么问题吗?也许我会就此提出一个具体问题。【参考方案6】:

如果您想采用这种方法并使用 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=childchild_id 在默认情况下充当子级,如果它没有被覆盖(我们想要的行为)。 write_only=True 使 child_id 可以写入,但由于 id 已经出现在 ChildSerializer 中,因此它不会出现在响应中。

【讨论】:

我收到以下错误消息: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.【参考方案7】:

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

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

【讨论】:

【参考方案8】:

这里的一些人已经设置了一种方法来保留一个字段,但在检索对象时仍然能够获取详细信息并仅使用 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,但在检索您创建的对象时(或任何时候)将返回完整的序列化嵌套模型。

【讨论】:

应该包含在 DRF 中。 :)【参考方案9】:

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

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 方法。我在对重复问题的回答中添加了这个:***.com/questions/26561640/… 谢谢!我浪费了一天的时间试图解决这个问题...所选答案对我不起作用...【参考方案10】:

我是这样解决这个问题的。

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/字典的子序列化器。在to_internal_value 中,如果子对象具有有效的 ID,我们会对其进行实例化,以便 DRF 可以进一步使用该对象。

【讨论】:

【参考方案11】:

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

【讨论】:

我遇到了和你一样的问题。你有没有想出一种方法让它在一个序列化器中运行? ***.com/questions/41248271【参考方案12】:

这里最好的解决方案是使用两个不同的字段:一个用于读取,另一个用于写入。如果不进行一些繁重的工作,就很难在单个字段中获得您正在寻找的东西。

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

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


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

【讨论】:

凯文感谢您的回答。我正在努力解决同样的问题。我在 ChildSerializer 中添加了两个字段。 parent = ParentSerializer(read_only=True)parent_id =serializers.PrimaryKeyRelatedField(...., write_only=True, ....) 我还添加了两个 parentparent_id 到 ChildSerializer 的字段。但是,我在响应中看不到任何 child_id 字段。实际上哪个好又方便,但我想知道这是什么原因?你有什么想法吗? 好答案。只是缺少一些示例代码,如瘦的答案(可能在下面)

以上是关于DRF:使用嵌套序列化程序的简单外键分配?的主要内容,如果未能解决你的问题,请参考以下文章

如何在实例中有自定义 .update() 以更新 DRF 可写嵌套序列化程序中的多对多关系时更新值

带有嵌套序列化程序的 Django 反向外键给出了多个结果

drf-外键字段设置及子序列化

Django Rest Framework:具有通用外键的可写嵌套序列化程序

在DRF中将外键序列化为列表

CSIC_716_20200221drf--自定义外键字段十大接口