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

2024-12-30 08:41:00
admin
原创
44
摘要:问题描述:使用 Django REST Framework,标准 ModelSerializer 将允许通过将 ID 作为整数发布来分配或更改 ForeignKey 模型关系。从嵌套序列化器中获取此行为的最简单的方法是什么?注意,我只谈论分配现有的数据库对象,而不是嵌套创建。我过去曾通过在序列化器中添加“id...

问题描述:

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

解决方案 1:

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

序列化器的完整表示

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

解决方案 2:

这里最好的解决方案是使用两个不同的字段:一个用于阅读,另一个用于写作。如果不做一些繁重的工作,很难在单个字段中找到您要查找的内容。

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

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


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

解决方案 3:

如果您想采用这种方法并使用 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

解决方案 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

解决方案 5:

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

解决方案 6:

有一个包可以实现这个功能!查看 Drf Extra Fields 包中的 PresentablePrimaryKeyRelatedField。

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

解决方案 7:

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

序列化器.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()

视图.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

解决方案 8:

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

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

解决方案 9:

在找到这个答案之前,我开始实现类似于JPG 的解决方案,并注意到它破坏了内置的 Django Rest Framework 模板。现在,这不是什么大问题(因为他们的解决方案通过请求/p​​ostman/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
        ])

下面是一个示例,Child Serializer 类:

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

解决方案 10:

根据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 \n                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) \n                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)

解决方案 11:

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

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

postman中get方法请求与响应的截图

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

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

postman中post方法请求与响应的截图

解决方案 12:

这是我到处都在使用的方法。这可能是最简单、最直接的方法,不需要任何黑客等,并且直接使用 DRF 而不需要费尽周折。很高兴听到对这种方法的不同意见。

在视图的 perform_create(或等效方法)中,获取与 POST 请求中发送的字段相对应的 FK 模型数据库对象,然后将发送到序列化器。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)

附言:请注意,我没有测试过上述代码片段,但它基于我在许多地方使用的模式,所以它应该可以按原样工作。

解决方案 13:

我没有修改你的代码。输入时需要 chhild_id,输出时需要 children,所以我把两者都设为可选。代码没有经过测试,我希望它能给你一些启发。

 class ParentSerializer(ModelSerializer):
    child_id = serializers.IntegerField(required=False)
    children = ChildSerializer(required=False) 

    def create(self, validated_data):
      child_id = validated_data['child_id']
    
      # it has to be a child object
      validated_data['children'] = Child.Object.get(id=child_id)
      return Parent.objects.create(**validated_data)

然后

 serializer = ParentSerializer(data={child_id=1})
 if serializer.is_valid():
   ...etc..

解决方案 14:

我遇到了类似的问题并通过覆盖来定制响应输出来解决to_representation()ParentSerializer

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

就我的情况来说,它看起来像这样:

models.py

class Category(models.Model):
    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class Tag(models.Model):
    name = models.CharField(max_length=255, unique=True)

    def __str__(self):
        return self.name


class Product(models.Model):
    name = models.CharField(max_length=255)
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
    tags = models.ManyToManyField(Tag, related_name='products', blank=True, null=True)

    def __str__(self):
        return self.name

serializers.py

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'


class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = '__all__'


class ProductSerializer(serializers.ModelSerializer):

    class Meta:
        model = Product
        fields = '__all__'

    def to_representation(self, instance):
        response = super().to_representation(instance)
        response['category'] = CategorySerializer(instance.category).data
        response['tags'] = TagSerializer(instance.tags, many=True).data

        return response

对于我来说,这非常有效POST,并且不会破坏方法many-to-onemany-to-many关系。

此外,depth=1Meta课堂上使用也会得到类似的反应:

class ProductSerializer(serializers.ModelSerializer):

    class Meta:
        model = Product
        fields = '__all__'
        depth = 1

    # def to_representation(self, instance):
    #     response = super().to_representation(instance)
    #     response['category'] = CategorySerializer(instance.category).data
    #     response['tags'] = TagSerializer(instance.tags, many=True).data
    #     response['tags_count'] = instance.tags.count()
    #     return response

但这种POST方法将会失效。

有关to_representation()方法的更多信息: https: //testdriven.io/blog/drf-serializers/#custom-outputs

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   990  
  在项目管理领域,CDCP(Certified Data Center Professional)认证评审是一个至关重要的环节,它不仅验证了项目团队的专业能力,还直接关系到项目的成功与否。在这一评审过程中,沟通技巧的运用至关重要。有效的沟通不仅能够确保信息的准确传递,还能增强团队协作,提升评审效率。本文将深入探讨CDCP...
华为IPD流程   26  
  IPD(Integrated Product Development,集成产品开发)是一种以客户需求为核心、跨部门协同的产品开发模式,旨在通过高效的资源整合和流程优化,提升产品开发的成功率和市场竞争力。在IPD培训课程中,掌握关键成功因素是确保团队能够有效实施这一模式的核心。以下将从五个关键成功因素展开讨论,帮助企业和...
IPD项目流程图   27  
  华为IPD(Integrated Product Development,集成产品开发)流程是华为公司在其全球化进程中逐步构建和完善的一套高效产品开发管理体系。这一流程不仅帮助华为在技术创新和产品交付上实现了质的飞跃,还为其在全球市场中赢得了显著的竞争优势。IPD的核心在于通过跨部门协作、阶段性评审和市场需求驱动,确保...
华为IPD   26  
  华为作为全球领先的通信技术解决方案提供商,其成功的背后离不开一套成熟的管理体系——集成产品开发(IPD)。IPD不仅是一种产品开发流程,更是一种系统化的管理思想,它通过跨职能团队的协作、阶段评审机制和市场需求驱动的开发模式,帮助华为在全球市场中脱颖而出。从最初的国内市场到如今的全球化布局,华为的IPD体系在多个领域展现...
IPD管理流程   53  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用