Django rest framework ซ้อนอ็อบเจกต์อ้างอิงตัวเอง


90

ฉันมีโมเดลที่มีลักษณะดังนี้:

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 แบบแบนของทุกหมวดหมู่ด้วย serializer:

class CategorySerializer(serializers.HyperlinkedModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()
    subcategories = serializers.ManyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

ตอนนี้สิ่งที่ฉันต้องการทำคือให้รายการหมวดหมู่ย่อยมีการแสดงประเภทย่อยแบบอินไลน์ json แทนรหัสของพวกเขา ฉันจะทำอย่างไรกับ django-rest-framework ฉันพยายามค้นหาในเอกสาร แต่ดูเหมือนว่าไม่สมบูรณ์

คำตอบ:


70

แทนที่จะใช้ ManyRelatedField ให้ใช้ Serializer ที่ซ้อนกันเป็นฟิลด์ของคุณ:

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

อันที่จริงตามที่คุณระบุไว้ข้างต้นไม่ถูกต้องนัก นี่เป็นการแฮ็กเล็กน้อย แต่คุณอาจลองเพิ่มช่องหลังจากประกาศ serializer แล้ว

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

CategorySerializer.base_fields['subcategories'] = CategorySerializer()

กลไกของการประกาศความสัมพันธ์แบบเรียกซ้ำเป็นสิ่งที่ต้องเพิ่ม


แก้ไข : โปรดทราบว่าขณะนี้มีแพ็กเกจของบุคคลที่สามซึ่งเกี่ยวข้องกับกรณีการใช้งานประเภทนี้โดยเฉพาะ ดูdjangorestframework-recursive


4
ตกลงมันใช้ได้กับความลึก = 1 จะเกิดอะไรขึ้นถ้าฉันมีระดับมากกว่าในโครงสร้างวัตถุ - หมวดหมู่มีหมวดหมู่ย่อยซึ่งมีหมวดหมู่ย่อย ฉันต้องการแทนต้นไม้ทั้งหมดของความลึกโดยพลการด้วยวัตถุแบบอินไลน์ ด้วยวิธีการของคุณฉันไม่สามารถกำหนดฟิลด์หมวดหมู่ย่อยใน SubCategorySerializer
Jacek Chmielewski

แก้ไขด้วยข้อมูลเพิ่มเติมเกี่ยวกับซีเรียลไลเซอร์แบบอ้างอิงตัวเอง
Tom Christie

4
สำหรับใครก็ตามที่ดูคำถามนี้ใหม่ฉันพบว่าสำหรับแต่ละระดับการเรียกซ้ำพิเศษฉันต้องทำซ้ำบรรทัดสุดท้ายในการแก้ไขครั้งที่สอง วิธีแก้ปัญหาแปลก ๆ แต่ดูเหมือนว่าจะได้ผล
Jeremy Blalock

1
@TomChristie คุณยังรับเด็กซ้ำที่รูทโธ่? ฉันจะหยุดสิ่งนี้ได้อย่างไร
Prometheus

20
ฉันแค่อยากจะชี้ให้เห็นว่า "base_fields" ใช้ไม่ได้อีกต่อไป ด้วย DRF 3.1.0 "_declared_fields" คือที่ที่เวทมนตร์อยู่
Travis Swientek

51

@ วิธีการแก้ปัญหาของ wjin ได้ทำงานที่ดีสำหรับผมจนผมอัพเกรด Django REST กรอบ 3.0.0 ซึ่ง deprecates to_native นี่คือโซลูชัน DRF 3.0 ของฉันซึ่งเป็นการปรับเปลี่ยนเล็กน้อย

สมมติว่าคุณมีโมเดลที่มีช่องอ้างอิงตัวเองเช่นความคิดเห็นที่เป็นชุดข้อความในคุณสมบัติที่เรียกว่า "การตอบกลับ" คุณมีการแสดงแผนภูมิต้นไม้ของเธรดความคิดเห็นนี้และคุณต้องการจัดลำดับแผนภูมิ

ขั้นแรกกำหนดคลาส 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, ....)

Easy peasy และคุณต้องใช้โค้ด 4 บรรทัดสำหรับโซลูชันที่ใช้ซ้ำได้

หมายเหตุ: หากโครงสร้างข้อมูลของคุณซับซ้อนกว่าแบบต้นไม้เช่นพูดว่าเป็น กราฟ acyclic ที่กำหนดทิศทาง (FANCY!) คุณสามารถลองใช้แพ็คเกจของ @ wjin - ดูวิธีแก้ปัญหาของเขา แต่ฉันไม่มีปัญหากับวิธีแก้ปัญหานี้สำหรับต้นไม้ที่ใช้ MPTTModel


1
line serializer = self.parent.parent .__ class __ (value, context = self.context) ทำอะไร เป็น to_representation () method หรือไม่?
Mauricio

บรรทัดนี้เป็นส่วนที่สำคัญที่สุด - ช่วยให้การแสดงฟิลด์อ้างอิงซีเรียลไลเซอร์ที่ถูกต้อง ในตัวอย่างนี้ฉันเชื่อว่ามันน่าจะเป็น CommentSerializer
Mark Chackerian

1
ฉันขอโทษ. ฉันไม่เข้าใจว่าโค้ดนี้ใช้ทำอะไร ฉันวิ่งและมันได้ผล แต่ฉันไม่รู้ว่ามันใช้งานได้จริงอย่างไร
Mauricio

ลองพิมพ์ข้อความเช่นprint self.parent.parent.__class__และprint self.parent.parent
Mark Chackerian

โซลูชันใช้งานได้ แต่เอาต์พุตการนับของซีเรียลไลเซอร์ของฉันไม่ถูกต้อง นับเฉพาะโหนดรูทเท่านั้น ความคิดใด ๆ ? เหมือนกันกับ djangorestframework-recursive
Lucas Veiga

39

อีกทางเลือกหนึ่งที่ใช้ได้กับ 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

ใน python 3 อาจเป็นเช่นนี้fields = super().get_fields()
Elinaldo Monteiro

นี่อาจไม่ใช่ตัวเลือกหากคุณต้องการใช้จุดสิ้นสุด OPTIONS ของมุมมองของคุณมันจะติดอยู่ในวงวนไม่สิ้นสุดหากฉันใช้วิธีนี้ โซลูชัน RecursiveField ใช้ได้ผลสำหรับฉันและใช้ซ้ำได้เช่นกัน
Prasad Pilla

30

มาเล่นเกมช้าที่นี่ แต่นี่คือทางออกของฉัน สมมติว่าฉันเป็นอนุกรมของ 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

2
คุณยังรับเด็กซ้ำที่รูทโธ่? ฉันจะหยุดสิ่งนี้ได้อย่างไร
Prometheus

ขอโทษ @Sputnik ฉันไม่เข้าใจว่าคุณหมายถึงอะไร สิ่งที่ฉันให้ที่นี่ใช้ได้กับกรณีที่คุณมีคลาสBlahและมีฟิลด์ที่เรียกว่าchild_blahsซึ่งประกอบด้วยรายการBlahวัตถุ
wjin

4
สิ่งนี้ใช้งานได้ดีจนกระทั่งฉันอัปเกรดเป็น DRF 3.0 ดังนั้นฉันจึงโพสต์รูปแบบ 3.0
Mark Chackerian

1
@ Falcon1 คุณสามารถกรอง Queryset และส่งเฉพาะโหนดรูทในมุมมองเช่นqueryset=Class.objects.filter(level=0). จัดการสิ่งที่เหลือเอง
chhantyal

15

ฉันสามารถบรรลุผลลัพธ์นี้ได้โดยใช้ไฟล์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 ทำให้เกิดการเรียกซ้ำไม่สิ้นสุดหรือต้องการภาวะแทรกซ้อนเพิ่มเติมเพื่อหลีกเลี่ยงการเรียกซ้ำและเลือกฟิลด์อย่างเหมาะสม
Louis

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íðir Orri Reynisson

คำตอบที่ดีและมีประโยชน์มาก เมื่อรับเด็กบน ModelSerializer คุณไม่สามารถระบุชุดสืบค้นสำหรับรับองค์ประกอบลูกได้ ในกรณีนี้คุณสามารถทำได้
Efrin

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
abhaga

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

โปรดสังเกตว่า CategorySerializer ในบรรทัดที่ 6 ถูกเรียกด้วยออบเจ็กต์และแอตทริบิวต์ many = True


น่าอัศจรรย์สิ่งนี้ได้ผลสำหรับฉัน อย่างไรก็ตามฉันคิดว่าif 'branches'ควรเปลี่ยนเป็นif 'subcategories'
vabada

6

ฉันคิดว่าฉันจะเข้าร่วมสนุก!

ผ่านwjinและMark Chackerianฉันได้สร้างวิธีแก้ปัญหาทั่วไปมากขึ้นซึ่งใช้ได้กับโมเดลที่เหมือนต้นไม้โดยตรงและโครงสร้างต้นไม้ที่มีแบบจำลองผ่าน ฉันไม่แน่ใจว่านี่เป็นคำตอบของมันเองหรือเปล่า แต่ฉันคิดว่าฉันอาจจะใส่มันไว้ที่ไหนสักแห่ง ฉันรวมตัวเลือก max_depth ซึ่งจะป้องกันการเรียกซ้ำแบบไม่สิ้นสุดที่เด็กระดับลึกที่สุดจะแสดงเป็น URLS (นั่นคือประโยคสุดท้ายหากคุณไม่ต้องการให้เป็น url)

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'])

1
นี่เป็นวิธีการแก้ปัญหาที่ละเอียดรอบคอบ แต่เป็นที่น่าสังเกตว่าelseประโยคของคุณตั้งสมมติฐานบางอย่างเกี่ยวกับมุมมอง ฉันต้องแทนที่ของฉันด้วยreturn value.pkดังนั้นมันจึงส่งคืนคีย์หลักแทนที่จะพยายามย้อนกลับดูมุมมอง
Soviut

4

ด้วย 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'

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

2

โซลูชันนี้เกือบจะคล้ายกับโซลูชันอื่น ๆ ที่โพสต์ไว้ที่นี่ แต่มีความแตกต่างเล็กน้อยในแง่ของปัญหาการทำซ้ำของเด็กที่ระดับราก (หากคุณคิดว่าเป็นปัญหา) ตัวอย่างเช่น

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 ในคลาส serializer: stackoverflow.com/questions/37985581/…
Aaron
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.