Django rest框架嵌套自引用对象
Posted
技术标签:
【中文标题】Django rest框架嵌套自引用对象【英文标题】:Django rest framework nested self-referential objects 【发布时间】:2012-11-02 20:02:47 【问题描述】:我的模型看起来像这样:
class Category(models.Model):
parentCategory = models.ForeignKey('self', blank=True, null=True, related_name='subcategories')
name = models.CharField(max_length=200)
description = models.CharField(max_length=500)
我设法用序列化器获得了所有类别的平面 json 表示:
class CategorySerializer(serializers.HyperlinkedModelSerializer):
parentCategory = serializers.PrimaryKeyRelatedField()
subcategories = serializers.ManyRelatedField()
class Meta:
model = Category
fields = ('parentCategory', 'name', 'description', 'subcategories')
现在我想做的是让子类别列表具有子类别的内联 json 表示,而不是它们的 id。我将如何使用 django-rest-framework 来做到这一点?我试图在文档中找到它,但似乎不完整。
【问题讨论】:
【参考方案1】:不要使用 ManyRelatedField,而是使用嵌套序列化器作为您的字段:
class SubCategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('name', 'description')
class CategorySerializer(serializers.ModelSerializer):
parentCategory = serializers.PrimaryKeyRelatedField()
subcategories = serializers.SubCategorySerializer()
class Meta:
model = Category
fields = ('parentCategory', 'name', 'description', 'subcategories')
如果您想处理任意嵌套的字段,您应该查看文档的customising the default fields 部分。您目前不能直接将序列化程序声明为自身的字段,但您可以使用这些方法来覆盖默认使用的字段。
class CategorySerializer(serializers.ModelSerializer):
parentCategory = serializers.PrimaryKeyRelatedField()
class Meta:
model = Category
fields = ('parentCategory', 'name', 'description', 'subcategories')
def get_related_field(self, model_field):
# Handles initializing the `subcategories` field
return CategorySerializer()
实际上,正如您所指出的,上述内容并不完全正确。 这有点小技巧,但您可以尝试在序列化程序已经声明后添加该字段。
class CategorySerializer(serializers.ModelSerializer):
parentCategory = serializers.PrimaryKeyRelatedField()
class Meta:
model = Category
fields = ('parentCategory', 'name', 'description', 'subcategories')
CategorySerializer.base_fields['subcategories'] = CategorySerializer()
需要添加一种声明递归关系的机制。
编辑:请注意,现在有一个第三方包可以专门处理这种用例。见djangorestframework-recursive。
【讨论】:
好的,这适用于 depth=1。如果我在对象树中有更多级别 - 类别有子类别而有子类别怎么办?我想用内联对象表示任意深度的整个树。使用您的方法,我无法在 SubCategorySerializer 中定义子类别字段。 编辑了关于自引用序列化程序的更多信息。 对于任何新查看此问题的人,我发现对于每个额外的递归级别,我必须在第二次编辑中重复最后一行。奇怪的解决方法,但似乎有效。 @TomChristie 你仍然让孩子从根本上重复吗?我怎样才能阻止这种情况? 我只想指出,“base_fields”不再有效。在 DRF 3.1.0 中,“_declared_fields”就是神奇之处。【参考方案2】:@wjin 的解决方案对我来说非常有用,直到我升级到 Django REST 框架 3.0.0,它弃用了 to_native。这是我的 DRF 3.0 解决方案,稍作修改。
假设您有一个具有自引用字段的模型,例如名为“回复”的属性中的线程化 cmets。你有这个评论线程的树表示,你想序列化树
首先,定义可重用的 RecursiveField 类
class RecursiveField(serializers.Serializer):
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
然后,对于您的序列化器,使用 RecursiveField 序列化“回复”的值
class CommentSerializer(serializers.Serializer):
replies = RecursiveField(many=True)
class Meta:
model = Comment
fields = ('replies, ....)
简单易行,只需 4 行代码即可获得可重复使用的解决方案。
注意:如果您的数据结构比树更复杂,例如 有向无环图(FANCY!),那么您可以尝试@wjin 的包——查看他的解决方案。但是对于基于 MPTTModel 的树,我对这个解决方案没有任何问题。
【讨论】:
行 serializer = self.parent.parent.__class__(value, context=self.context) 是做什么的。是 to_representation() 方法吗? 这一行是最重要的部分——它允许字段的表示引用正确的序列化程序。在这个例子中,我相信它会是 CommentSerializer。 对不起。我无法理解这段代码在做什么。我运行它并且它有效。但我不知道它实际上是如何工作的。 尝试加入一些打印语句,例如print self.parent.parent.__class__
和print self.parent.parent
这会给我一个错误:self.parent.parent 是 None。下面有一个更简单的解决方案,使用 get_fields 似乎可以解决问题【参考方案3】:
另一个适用于 Django REST Framework 3.3.2 的选项:
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'parentid', 'subcategories')
def get_fields(self):
fields = super(CategorySerializer, self).get_fields()
fields['subcategories'] = CategorySerializer(many=True)
return fields
【讨论】:
为什么这不是公认的答案?完美运行。 这个工作非常简单,我比发布的其他解决方案更容易完成这个工作。 这个解决方案不需要额外的类,并且比parent.parent.__class__
的东西更容易理解。我最喜欢它。
如果您想使用视图的 OPTIONS 端点,这可能不是一个选项,如果我使用这种方法,它会陷入无限循环。 RecursiveField 解决方案对我有用,也可以重复使用。
在 DRF 3.12 中生成 OpenAPI 模式时,此方法会导致 RecursionError
。 RecursiveField
方法很迟钝,但可以避免这个问题。【参考方案4】:
这里的游戏迟到了,但这是我的解决方案。假设我正在序列化一个 Blah,其中有多个 Blah 类型的孩子。
class RecursiveField(serializers.Serializer):
def to_native(self, value):
return self.parent.to_native(value)
使用此字段,我可以序列化具有许多子对象的递归定义的对象
class BlahSerializer(serializers.Serializer):
name = serializers.Field()
child_blahs = RecursiveField(many=True)
我为 DRF3.0 写了一个递归字段,并为 pip 打包 https://pypi.python.org/pypi/djangorestframework-recursive/
【讨论】:
适用于序列化 MPTTModel。不错! 您仍然让孩子在根部重复吗?我怎样才能阻止这种情况? 对不起@Sputnik 我不明白你的意思。我在这里给出的内容适用于你有一个类Blah
并且它有一个名为 child_blahs
的字段,其中包含一个 Blah
对象列表。
在我升级到 DRF 3.0 之前效果很好,所以我发布了 3.0 版本。
@Falcon1 您可以过滤查询集并仅在 queryset=Class.objects.filter(level=0)
等视图中传递根节点。它自己处理其余的事情。【参考方案5】:
我能够使用serializers.SerializerMethodField
实现此结果。我不确定这是否是最好的方法,但对我有用:
class CategorySerializer(serializers.ModelSerializer):
subcategories = serializers.SerializerMethodField(
read_only=True, method_name="get_child_categories")
class Meta:
model = Category
fields = [
'name',
'category_id',
'subcategories',
]
def get_child_categories(self, obj):
""" self referral field """
serializer = CategorySerializer(
instance=obj.subcategories_set.all(),
many=True
)
return serializer.data
【讨论】:
对我来说,这归结为在此解决方案和 yprez's solution 之间进行选择。它们比之前发布的解决方案更清晰、更简单。这里的解决方案赢了,因为我发现这是解决这里 OP 提出的问题的最佳方法同时支持this solution for dynamically selecting fields to be serialized。 Yprez 的解决方案会导致无限递归或需要额外的复杂性来避免递归并正确选择字段。【参考方案6】:另一种选择是在序列化模型的视图中递归。这是一个例子:
class DepartmentSerializer(ModelSerializer):
class Meta:
model = models.Department
class DepartmentViewSet(ModelViewSet):
model = models.Department
serializer_class = DepartmentSerializer
def serialize_tree(self, queryset):
for obj in queryset:
data = self.get_serializer(obj).data
data['children'] = self.serialize_tree(obj.children.all())
yield data
def list(self, request):
queryset = self.get_queryset().filter(level=0)
data = self.serialize_tree(queryset)
return Response(data)
def retrieve(self, request, pk=None):
self.object = self.get_object()
data = self.serialize_tree([self.object])
return Response(data)
【讨论】:
这太棒了,我有一个任意深的树需要序列化,这就像一个魅力! 很好且非常有用的答案。在 ModelSerializer 上获取子元素时,您无法指定用于获取子元素的查询集。在这种情况下,您可以这样做。【参考方案7】:我最近遇到了同样的问题,并提出了一个迄今为止似乎有效的解决方案,即使对于任意深度也是如此。 解决方案是对 Tom Christie 的解决方案进行小修改:
class CategorySerializer(serializers.ModelSerializer):
parentCategory = serializers.PrimaryKeyRelatedField()
def convert_object(self, obj):
#Add any self-referencing fields here (if not already done)
if not self.fields.has_key('subcategories'):
self.fields['subcategories'] = CategorySerializer()
return super(CategorySerializer,self).convert_object(obj)
class Meta:
model = Category
#do NOT include self-referencing fields here
#fields = ('parentCategory', 'name', 'description', 'subcategories')
fields = ('parentCategory', 'name', 'description')
#This is not needed
#CategorySerializer.base_fields['subcategories'] = CategorySerializer()
我不确定它能否在任何情况下可靠地工作,不过...
【讨论】:
从 2.3.8 开始,没有 convert_object 方法。但同样的事情可以通过覆盖 to_native 方法来完成。【参考方案8】:这是对适用于 drf 3.0.5 和 django 2.7.4 的 caipirginka 解决方案的改编:
class CategorySerializer(serializers.ModelSerializer):
def to_representation(self, obj):
#Add any self-referencing fields here (if not already done)
if 'branches' not in self.fields:
self.fields['subcategories'] = CategorySerializer(obj, many=True)
return super(CategorySerializer, self).to_representation(obj)
class Meta:
model = Category
fields = ('id', 'description', 'parentCategory')
请注意,第 6 行中的 CategorySerializer 是使用对象和 many=True 属性调用的。
【讨论】:
太棒了,这对我有用。不过我觉得if 'branches'
应该改成if 'subcategories'
谢谢,为我工作,无需在此处传递对象CategorySerializer(obj, many=True)
。【参考方案9】:
我想我会参与其中!
通过wjin 和Mark Chackerian 我创建了一个更通用的解决方案,它适用于直接树状模型和具有直通模型的树结构。我不确定这是否属于它自己的答案,但我想我不妨把它放在某个地方。我包含了一个 max_depth 选项,它可以防止无限递归,在最深的层次上,子项表示为 URL(如果您不希望它不是 url,那就是最后的 else 子句)。
from rest_framework.reverse import reverse
from rest_framework import serializers
class RecursiveField(serializers.Serializer):
"""
Can be used as a field within another serializer,
to produce nested-recursive relationships. Works with
through models, and limited and/or arbitrarily deep trees.
"""
def __init__(self, **kwargs):
self._recurse_through = kwargs.pop('through_serializer', None)
self._recurse_max = kwargs.pop('max_depth', None)
self._recurse_view = kwargs.pop('reverse_name', None)
self._recurse_attr = kwargs.pop('reverse_attr', None)
self._recurse_many = kwargs.pop('many', False)
super(RecursiveField, self).__init__(**kwargs)
def to_representation(self, value):
parent = self.parent
if isinstance(parent, serializers.ListSerializer):
parent = parent.parent
lvl = getattr(parent, '_recurse_lvl', 1)
max_lvl = self._recurse_max or getattr(parent, '_recurse_max', None)
# Defined within RecursiveField(through_serializer=A)
serializer_class = self._recurse_through
is_through = has_through = True
# Informed by previous serializer (for through m2m)
if not serializer_class:
is_through = False
serializer_class = getattr(parent, '_recurse_next', None)
# Introspected for cases without through models.
if not serializer_class:
has_through = False
serializer_class = parent.__class__
if is_through or not max_lvl or lvl <= max_lvl:
serializer = serializer_class(
value, many=self._recurse_many, context=self.context)
# Propagate hereditary attributes.
serializer._recurse_lvl = lvl + is_through or not has_through
serializer._recurse_max = max_lvl
if is_through:
# Delay using parent serializer till next lvl.
serializer._recurse_next = parent.__class__
return serializer.data
else:
view = self._recurse_view or self.context['request'].resolver_match.url_name
attr = self._recurse_attr or 'id'
return reverse(view, args=[getattr(value, attr)],
request=self.context['request'])
【讨论】:
这是一个非常彻底的解决方案,但是值得注意的是,您的else
子句对视图做出了某些假设。我不得不用return value.pk
替换我的,所以它返回了主键,而不是尝试反向查找视图。【参考方案10】:
使用 Django REST framework 3.3.1,我需要以下代码来将子类别添加到类别中:
models.py
class Category(models.Model):
id = models.AutoField(
primary_key=True
)
name = models.CharField(
max_length=45,
blank=False,
null=False
)
parentid = models.ForeignKey(
'self',
related_name='subcategories',
blank=True,
null=True
)
class Meta:
db_table = 'Categories'
序列化器.py
class SubcategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'parentid')
class CategorySerializer(serializers.ModelSerializer):
subcategories = SubcategorySerializer(many=True, read_only=True)
class Meta:
model = Category
fields = ('id', 'name', 'parentid', 'subcategories')
【讨论】:
【参考方案11】:此解决方案与此处发布的其他解决方案几乎相似,但在根级别的子级重复问题方面略有不同(如果您认为这是一个问题)。举个例子
class RecursiveSerializer(serializers.Serializer):
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
class CategoryListSerializer(ModelSerializer):
sub_category = RecursiveSerializer(many=True, read_only=True)
class Meta:
model = Category
fields = (
'name',
'slug',
'parent',
'sub_category'
)
如果你有这个观点
class CategoryListAPIView(ListAPIView):
queryset = Category.objects.all()
serializer_class = CategoryListSerializer
这将产生以下结果,
[
"name": "parent category",
"slug": "parent-category",
"parent": null,
"sub_category": [
"name": "child category",
"slug": "child-category",
"parent": 20,
"sub_category": []
]
,
"name": "child category",
"slug": "child-category",
"parent": 20,
"sub_category": []
]
这里的parent category
有一个child category
,而json 表示正是我们想要的。
但您可以看到在根级别重复了 child category
。
正如一些人在上面发布的答案的评论部分中询问 我们如何在根级别停止这种子重复,只需使用 parent=None
过滤您的查询集,如下所示
class CategoryListAPIView(ListAPIView):
queryset = Category.objects.filter(parent=None)
serializer_class = CategoryListSerializer
它会解决问题。
注意:这个答案可能与问题没有直接关系,但问题在某种程度上是相关的。这种使用RecursiveSerializer
的方法也很昂贵。如果您使用其他容易提高性能的选项会更好。
【讨论】:
带有过滤器的查询集对我造成了错误。但这有助于摆脱重复的领域。重写序列化器类中的 to_representation 方法:***.com/questions/37985581/…以上是关于Django rest框架嵌套自引用对象的主要内容,如果未能解决你的问题,请参考以下文章