Django 나머지 프레임 워크 중첩 된 자체 참조 개체


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)

serializer를 사용하여 모든 범주의 평면 json 표현을 얻을 수있었습니다.

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

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

이제 내가하고 싶은 것은 하위 카테고리 목록이 ID 대신 하위 카테고리의 인라인 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를 참조하십시오 .


3
좋아, 이것은 depth = 1에 대해 작동합니다. 개체 트리에 더 많은 수준이있는 경우-범주에 하위 범주가있는 하위 범주가있는 경우 어떻게합니까? 인라인 객체로 임의의 깊이의 전체 트리를 표현하고 싶습니다. 귀하의 접근 방식을 사용하여 SubCategorySerializer에서 하위 범주 필드를 정의 할 수 없습니다.
Jacek Chmielewski

자체 참조 serializer에 대한 자세한 정보로 편집되었습니다.
Tom Christie

이제 KeyError at /api/category/ 'subcategories'. 초고속 답변에 대한 Btw 감사합니다 :)
Jacek Chmielewski

4
이 질문을 새로 보는 사람은 각 추가 재귀 수준에 대해 두 번째 편집에서 마지막 줄을 반복해야한다는 것을 알았습니다. 이상한 해결 방법이지만 작동하는 것 같습니다.
Jeremy Blalock

19
"base_fields"가 더 이상 작동하지 않는다는 점을 지적하고 싶습니다. DRF 3.1.0에서는 "_declared_fields"가 마법이있는 곳입니다.
Travis Swientek 2015 년

50

@wjin의 솔루션은 to_native 를 사용하지 않는 Django REST 프레임 워크 3.0.0으로 업그레이드 할 때까지 잘 작동 했습니다. . 다음은 약간 수정 된 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

그런 다음 serializer의 경우 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 () 메서드입니까?
Mauricio

이 줄은 가장 중요한 부분입니다. 필드의 표현이 올바른 serializer를 참조 할 수 있도록합니다. 이 예에서는 CommentSerializer라고 생각합니다.
Mark Chackerian

1
죄송 해요. 이 코드가 무엇을하는지 이해할 수 없습니다. 나는 그것을 실행하고 작동합니다. 그러나 나는 그것이 실제로 어떻게 작동하는지 전혀 모른다.
Mauricio

같은 일부 인쇄 문에서 퍼팅 시도 print self.parent.parent.__class__print self.parent.parent
마크 Chackerian

솔루션은 작동하지만 내 직렬 변환기의 카운트 출력이 잘못되었습니다. 루트 노드 만 계산합니다. 어떤 아이디어? djangorestframework-recursive와 동일합니다.
Lucas Veiga 2017 년

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

@Sputnik 죄송합니다 무슨 말인지 이해가 안 돼요. 여기서 제가 제공 한 것은 클래스가 Blah있고 객체 child_blahs목록으로 구성된 필드가 있는 경우에 적용됩니다 Blah.
wjin

4
이것은 DRF 3.0으로 업그레이드 할 때까지 훌륭하게 작동했기 때문에 3.0 변형을 게시했습니다.
Mark Chackerian

1
@ Falcon1 쿼리 셋을 필터링하고 queryset=Class.objects.filter(level=0). 나머지는 자체적으로 처리합니다.
chhantyal

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의 솔루션은 무한 재귀를 유발 하거나 재귀 를 피하고 필드를 적절하게 선택하기 위해 추가 합병증이 필요합니다.
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 2014-06-10

좋고 매우 유용한 답변입니다. ModelSerializer에서 자식을 가져올 때 자식 요소를 가져 오기위한 쿼리 집합을 지정할 수 없습니다. 이 경우 그렇게 할 수 있습니다.
티쉬 린는 에프 린

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

이것은 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'
vabada

5

나는 내가 재미에 동참 할 것이라고 생각했다!

wjinMark Chackerian을 통해 저는보다 일반적인 솔루션을 만들었습니다.이 솔루션은 직접 나무와 유사한 모델과 관통 모델이있는 나무 구조에서 작동합니다. 이것이 자신의 대답에 속하는지 확실하지 않지만 어딘가에 두는 것이 좋을 것이라고 생각했습니다. 무한 재귀를 방지하는 max_depth 옵션을 포함 시켰습니다. 가장 깊은 수준에서 자식은 URLS로 표시됩니다 (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뷰를 역방향 조회하는 대신 기본 키를 반환하도록 내 것을 대체 해야했습니다.
Soviut

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 categoryhas a child category와 json 표현은 정확히 우리가 표현하고자하는 것입니다.

그러나 child category루트 수준에서의 반복이 있음을 알 수 있습니다 .

일부 사람들은 위의 게시 된 답변의 주석 섹션 에서 루트 수준 에서이 자식 반복어떻게 중지 할 수 있는지 묻고 parent=None있으므로 다음과 같이 쿼리 세트를로 필터링하십시오.

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.filter(parent=None)
    serializer_class = CategoryListSerializer

문제를 해결할 것입니다.

참고 :이 답변은 질문과 직접적인 관련이 없을 수 있지만 문제는 어떻게 든 관련이 있습니다. 또한 이러한 사용 방법 RecursiveSerializer은 비용이 많이 듭니다. 성능이 좋은 다른 옵션을 사용하면 더 좋습니다.


필터가있는 쿼리 세트로 인해 오류가 발생했습니다. 그러나 이것은 반복되는 필드를 제거하는 데 도움이되었습니다. serializer 클래스에서 to_representation 메서드 재정의 : stackoverflow.com/questions/37985581/…
Aaron
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.