DRF:使用嵌套序列化器进行简单的外键分配?
- 2024-12-30 08:41:00
- admin 原创
- 44
问题描述:
使用 Django REST Framework,标准 ModelSerializer 将允许通过将 ID 作为整数发布来分配或更改 ForeignKey 模型关系。
从嵌套序列化器中获取此行为的最简单的方法是什么?
注意,我只谈论分配现有的数据库对象,而不是嵌套创建。
我过去曾通过在序列化器中添加“id”字段以及使用自定义方法解决这个问题create
,update
但对我来说,这是一个看似简单且频繁出现的问题,所以我很好奇想知道最好的解决方法。
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
}
截屏
解决方案 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 模板。现在,这不是什么大问题(因为他们的解决方案通过请求/postman/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-one
和many-to-many
关系。
此外,depth=1
在Meta
课堂上使用也会得到类似的反应:
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
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理必备:盘点2024年13款好用的项目管理软件