Django rest框架嵌套了自指对象


88

我有看起来像这样的模型:

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我该怎么做?我试图在文档中找到它,但它似乎不完整。

Answers:


70

而不是使用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')

如果要处理任意嵌套的字段,则应查看自定义文档的默认字段部分。您目前无法直接将序列化器声明为自身的字段,但是可以使用这些方法来覆盖默认情况下使用的字段。

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


3
好的,这适用于depth = 1。如果我在对象树中有更多级别,该怎么办?类别具有子类别,而子类别又具有子类别?我想用内联对象表示任意深度的整个树。使用您的方法,我无法在SubCategorySerializer中定义子类别字段。
Jacek Chmielewski 2012年

编辑了有关自引用序列化器的更多信息。
汤姆·克里斯蒂

现在我明白了KeyError at /api/category/ 'subcategories'。顺便说一句,谢谢您的超快速回复:)
Jacek Chmielewski 2012年

4
对于任何新浏览此问题的人,我发现对于每个额外的递归级别,我都必须在第二次编辑中重复最后一行。奇怪的解决方法,但似乎可行。
杰里米·布拉洛克

19
我只想指出,“ base_fields”不再有效。在DRF 3.1.0中,“ _ declared_fields”是神奇之处。
Travis Swientek'Mar

50

@wjin的解决方案对我来说非常有效,直到我升级到不推荐使用to_native的Django REST Framework 3.0.0 。这是我的DRF 3.0解决方案,它做了一些修改。

假设您有一个带有自我引用字段的模型,例如,名为“ replies”的属性中的线程注释。您具有该注释线程的树表示形式,并且想要序列化该树

首先,定义您的可重用RecursiveField类

class RecursiveField(serializers.Serializer):
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

然后,对于您的序列化程序,使用RecursiveField序列化“ replies”的值

class CommentSerializer(serializers.Serializer):
    replies = RecursiveField(many=True)

    class Meta:
        model = Comment
        fields = ('replies, ....)

简便易行,您只需4行代码即可获得可重复使用的解决方案。

注意:如果您的数据结构比树还复杂,比如说有 向无环图(FANCY!),那么您可以尝试@wjin的软件包-请参阅他的解决方案。但是对于基于MPTTModel的树,该解决方案没有任何问题。


1
行serializer = self.parent.parent .__ class __(value,context = self.context)是做什么的。是to_representation()方法吗?
毛里西奥

这行是最重要的部分-它允许字段的表示引用正确的序列化程序。在此示例中,我相信它将是CommentSerializer。
Mark Chackerian '16

1
对不起。我不明白这段代码在做什么。我运行了它并且起作用了。但是我不知道它是如何工作的。
毛里西奥

尝试把像一些打印语句print self.parent.parent.__class__print self.parent.parent
马克Chackerian

解决方案有效,但是我的序列化器的计数输出错误。它仅计算根节点。有任何想法吗?djangorestframework-recursive也是一样。
卢卡斯·韦加

37

适用于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

6
为什么这不是公认的答案?完美运作。
Karthik RP

5
这非常简单,与其他发布的解决方案相比,我可以更轻松地完成此工作。
Nick BL

该解决方案不需要额外的类,并且比其他parent.parent.__class__东西更容易理解。我最喜欢
SergiyKolesnikov

27

在这里比赛晚了,但是这是我的解决方案。假设我要序列化一个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/


1
与序列化MPTTModel一起使用。真好!
Mark Chackerian 2014年

2
您仍然让孩子从头开始吗?我该如何阻止呢?
Prometheus 2014年

抱歉@Sputnik,我不明白您的意思。我在这里给出的内容适用于您拥有一个类Blah并且它具有一个child_blahsBlah对象列表组成的字段的情况。
wjin 2014年

4
在我升级到DRF 3.0之前,这一直很好,所以我发布了3.0版本。
Mark Chackerian 2014年

1
@ Falcon1您可以过滤查询集,并且仅在诸如之类的视图中传递根节点queryset=Class.objects.filter(level=0)。它自己处理其余的事情。
chhantyal,2015年

13

我能够使用来实现此结果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

1
对我而言,只能在此解决方案和yprez的解决方案之间进行选择。与先前发布的解决方案相比,它们既清晰又简单。这里的解决方案胜出是因为我发现这是解决OP提出的问题的最佳方法,同时支持此解决方案以动态选择要序列化的字段。Yprez的解决方案会导致无限递归需要额外的复杂性来避免递归并正确选择字段。
路易

9

另一个选择是在序列化模型的视图中递归。这是一个例子:

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)

太好了,我有一棵任意深的树,我需要对其进行序列化,并且这就像一个魅力!
VíðirOrri(奥瑞)Reynisson

很好,非常有用的答案。在ModelSerializer上获取子元素时,您无法指定用于获取子元素的查询集。在这种情况下,您可以这样做。
Efrin 2014年

8

最近,我遇到了同样的问题,并提出了一个似乎可行的解决方案,即使对于任意深度也是如此。解决方案是对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()

我不确定它在任何情况下都可以可靠地工作,但是...


1
从2.3.8开始,没有convert_object方法。但是,可以通过重写to_native方法来完成同一件事。
2013年

6

这是对caipirginka解决方案的改编,该解决方案适用于drf 3.0.5和Django 2.7.4:

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

5

我以为我会加入其中!

通过wjinMark 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所以它返回主键,而不是试图反向查找视图。
苏联

4

使用Django REST框架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'

serializers.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')

1

此解决方案与此处发布的其他解决方案几乎相似,但是在根本上,就子重发问题而言(如果您认为它是一个问题),则存在细微差异。举个例子

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很昂贵。如果您使用其他倾向于性能的选项,那就更好了。


带过滤器的queryset对我造成了错误。但这有助于摆脱重复的领域。:覆盖的序列化器类to_representation方法 stackoverflow.com/questions/37985581/...
阿伦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.