Django rest çerçevesi, kendinden referanslı nesneler iç içe


90

Şuna benzeyen modelim var:

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)

Serileştirici ile tüm kategorilerin düz json temsilini almayı başardım:

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

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

Şimdi yapmak istediğim şey, alt kategoriler listesinin alt kategorilerin kimlikleri yerine satır içi json temsiline sahip olmasıdır. Bunu django-rest-framework ile nasıl yapabilirim? Belgelerde bulmaya çalıştım ama eksik görünüyor.

Yanıtlar:


70

ManyRelatedField kullanmak yerine, alanınız olarak iç içe geçmiş bir serileştirici kullanın:

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

Keyfi olarak iç içe geçmiş alanlarla uğraşmak istiyorsanız , belgelerin varsayılan alanları özelleştirme bölümüne bir göz atmalısınız . Şu anda bir serileştiriciyi doğrudan kendi başına bir alan olarak ilan edemezsiniz, ancak bu yöntemleri varsayılan olarak hangi alanların kullanıldığını geçersiz kılmak için kullanabilirsiniz.

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

Aslında, yukarıda belirttiğiniz gibi tam olarak doğru değil. Bu biraz zor ama serileştirici zaten bildirildikten sonra alanı eklemeyi deneyebilirsiniz.

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

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

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

Özyinelemeli ilişkiler bildirme mekanizması, eklenmesi gereken bir şeydir.


Düzenleme : Artık, özellikle bu tür kullanım durumuyla ilgilenen üçüncü taraf bir paket bulunduğunu unutmayın. Djangorestframework-recursive konusuna bakın .


4
Tamam, bu derinlik = 1 için çalışır. Nesne ağacında daha fazla düzeyim varsa ne olur - kategorinin alt kategorisi olan alt kategorisi vardır? Satır içi nesnelerle gelişigüzel derinliğin tüm ağacını temsil etmek istiyorum. Yaklaşımınızı kullanarak SubCategorySerializer'da alt kategori alanı tanımlayamıyorum.
Jacek Chmielewski

Kendine referanslı serileştiriciler hakkında daha fazla bilgi ile düzenlendi.
Tom Christie

4
Bu soruyu yeni arayan herkes için, her ekstra özyinelemeli seviye için, ikinci düzenlemedeki son satırı tekrar etmem gerektiğini fark ettim. Garip bir çözüm, ancak işe yarıyor gibi görünüyor.
Jeremy Blalock

1
@TomChristie Çocuğu hala kök tho'da tekrar ediyor musunuz? Bunu nasıl durdurabilirim?
Prometheus

20
"Base_fields" artık çalışmıyor, belirtmek isterim. DRF 3.1.0 ile "_declared_fields" sihrin olduğu yerdir.
Travis Swientek

50

@ wjin'in çözümü, Django REST framework 3.0.0'a yükseltme yapana kadar benim için harika çalışıyordu, bu da to_native'i kullanımdan kaldırıyor . İşte küçük bir değişiklik olan DRF 3.0 çözümüm.

Kendi kendine referans alanlı bir modeliniz olduğunu varsayalım, örneğin "yanıtlar" adlı bir özellikteki zincir halinde yorumlar. Bu yorum dizisinin ağaç temsiline sahipsiniz ve ağacı serileştirmek istiyorsunuz

İlk olarak, yeniden kullanılabilir RecursiveField sınıfınızı tanımlayın

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

Ardından, serileştiriciniz için "yanıtlar" ın değerini serileştirmek için RecursiveField kullanın.

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

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

Kolay peasy ve yeniden kullanılabilir bir çözüm için yalnızca 4 satır koda ihtiyacınız var.

NOT: Veri yapınız bir ağaçtan daha karmaşıksa, örneğin yönlendirilmiş döngüsel olmayan bir grafik (FANCY!) Ancak MPTTM model tabanlı ağaçlar için bu çözümle ilgili herhangi bir sorun yaşamadım.


1
Satır serileştirici = self.parent.parent .__ sınıf __ (değer, bağlam = self.context) ne yapar? To_representation () yöntemi mi?
Mauricio

Bu satır en önemli kısımdır - alanın temsilinin doğru serileştiriciye başvurmasına izin verir. Bu örnekte, CommentSerializer olacağına inanıyorum.
Mark Chackerian

1
Üzgünüm. Bu kodun ne yaptığını anlayamadım. Ben çalıştırdım ve işe yarıyor. Ama gerçekte nasıl çalıştığı hakkında hiçbir fikrim yok.
Mauricio

Gibi bazı baskı tablolara sokmayı dene print self.parent.parent.__class__veprint self.parent.parent
Mark Chackerian

Çözüm çalışıyor ancak serileştiricimin sayım çıktısı yanlış. Yalnızca kök düğümleri sayar. Herhangi bir fikir? Djangorestframework-recursive ile aynıdır.
Lucas Veiga

39

Django REST Framework 3.3.2 ile çalışan başka bir seçenek:

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
Neden kabul edilen cevap bu değil? Mükemmel çalışıyor.
Karthik RP

5
Bu çok basit çalışıyor, bunu çalıştırmak için yayınlanan diğer çözümlerden çok daha kolay zaman geçirdim.
Nick BL

Bu çözümün fazladan derslere ihtiyacı yoktur ve anlaşılması parent.parent.__class__şeylerden daha kolaydır . Ben en çok beğendim.
SergiyKolesnikov

Python 3'te şu şekilde olabilir:fields = super().get_fields()
Elinaldo Monteiro

30

Burada oyuna geç kaldım ama işte çözümüm. Diyelim ki bir Blah'ı, aynı zamanda Blah tipi birden fazla çocukla seri hale getiriyorum.

    class RecursiveField(serializers.Serializer):
        def to_native(self, value):
            return self.parent.to_native(value)

Bu alanı kullanarak, birçok alt nesneye sahip özyinelemeli olarak tanımlanmış nesnelerimi serileştirebilirim

    class BlahSerializer(serializers.Serializer):
        name = serializers.Field()
        child_blahs = RecursiveField(many=True)

DRF3.0 için yinelemeli bir alan yazdım ve pip için paketledim https://pypi.python.org/pypi/djangorestframework-recursive/


1
Bir MPTTM modelini serileştirme ile çalışır. Güzel!
Mark Chackerian

2
Hala çocuğu tho'nun kökünde tekrarlıyor musunuz? Bunu nasıl durdurabilirim?
Prometheus

Üzgünüm @Sputnik Ne demek istediğini anlamıyorum. Burada verdiklerim, bir sınıfınızın olduğu Blahve nesnelerin child_blahsbir listesinden oluşan bir alanı olduğu durumlarda işe Blahyarar.
wjin

4
Bu, DRF 3.0'a yükseltene kadar harika çalışıyordu, bu yüzden bir 3.0 varyasyonu yayınladım.
Mark Chackerian

1
@ Falcon1 Sorgu setini filtreleyebilir ve sadece queryset=Class.objects.filter(level=0). Geri kalan şeyleri kendisi halleder.
chhantyal

15

Bu sonucu a kullanarak başardım serializers.SerializerMethodField. Bunun en iyi yol olduğundan emin değilim, ama benim için çalıştı:

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
Benim için bu çözüm ve yprez'in çözümü arasında bir seçim oldu . Daha önce yayınlanan çözümlerden hem daha net hem de daha basit. Buradaki çözüm kazandı, çünkü burada OP tarafından sunulan sorunu çözmenin en iyi yolu olduğunu ve aynı zamanda serileştirilecek alanların dinamik olarak seçilmesi için bu çözümü desteklediğini buldum . Yprez'in çözümü sonsuz bir özyinelemeye neden olur veya özyinelemeden kaçınmak ve alanları uygun şekilde seçmek için ek komplikasyonlar gerektirir.
Louis

9

Diğer bir seçenek, modelinizi serileştiren görünümde yinelemektir. İşte bir örnek:

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)

Bu harika, serileştirmem gereken keyfi derin bir ağaca sahiptim ve bu harika çalıştı!
Víðir Orri Reynisson

İyi ve çok faydalı cevap. ModelSerializer'da alt öğeleri alırken, alt öğeleri almak için bir sorgu kümesi belirtemezsiniz. Bu durumda bunu yapabilirsiniz.
Efrin

8

Geçenlerde aynı sorunu yaşadım ve keyfi derinlik için bile şimdiye kadar işe yarayan bir çözüm buldum. Çözüm, Tom Christie'den küçük bir değişiklik:

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

Yine de her durumda güvenilir bir şekilde çalışabileceğinden emin değilim ...


1
2.3.8'den itibaren, convert_object yöntemi yoktur. Ancak aynı şey, to_native yöntemini geçersiz kılarak da yapılabilir.
abhaga

6

Bu, drf 3.0.5 ve django 2.7.4 üzerinde çalışan caipirginka çözümünden bir uyarlamadır:

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. satırdaki CategorySerializer'ın nesne ve many = True niteliğiyle çağrıldığını unutmayın.


Harika, bu benim için çalıştı. Ancak, if 'branches'şu şekilde değiştirilmelidir diye düşünüyorumif 'subcategories'
vabada

6

Eğlenceye katılacağımı düşündüm!

Via wjin ve Mark Chackerian ben direkt için çalışan daha genel bir çözüm yarattı ağacın benzeri modelleri ve ağaç yapıları modeli ile var olan. Bunun kendi cevabına ait olup olmadığından emin değilim ama bir yere koyabilirim diye düşündüm. Sonsuz özyinelemeyi önleyecek bir max_depth seçeneği ekledim, en derin düzeyde çocuklar URLS olarak temsil edilir (eğer url olmasaydı bu son else cümlesi).

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
Bu çok kapsamlı bir çözüm, ancak, cümlenizin elsegörüş hakkında belirli varsayımlar yaptığını belirtmekte fayda var . Benimkini bununla değiştirmek zorunda kaldım, return value.pkböylece görünümü tersine çevirmeye çalışmak yerine birincil anahtarları döndürdü.
Soviut

4

Django REST çerçevesi 3.3.1 ile, kategorilere alt kategoriler eklemek için aşağıdaki koda ihtiyacım vardı:

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

Bu çözüm, burada yayınlanan diğer çözümlerle neredeyse benzerdir ancak kök düzeyinde çocuk tekrarı sorunu açısından küçük bir farka sahiptir (bunun bir sorun olduğunu düşünüyorsanız). Örnek olarak

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

ve eğer bu görüşe sahipsen

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.all()
    serializer_class = CategoryListSerializer

Bu, aşağıdaki sonucu verecektir,

[
{
    "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": []
}
]

Burada parent categoryhas a child categoryve json temsili tam olarak temsil edilmesini istediğimiz şeydir.

ancak child categorykök seviyesinde bir tekrarının olduğunu görebilirsiniz .

Bazı insanlar yukarıda yayınlanan yorum bölümlerinde bu çocuk tekrarını kök seviyesinde nasıl durdurabileceğimizi sorduğundan , sorgu kümenizi parent=Noneaşağıdaki gibi filtreleyin.

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

sorunu çözecektir.

NOT: Bu yanıt doğrudan soruyla ilgili olmayabilir, ancak sorun bir şekilde ilişkilidir. Ayrıca bu kullanma yaklaşımı RecursiveSerializerpahalıdır. Performansa eğilimli diğer seçenekleri kullanırsanız daha iyi olur.


Filtreli sorgu kümesi benim için bir hataya neden oldu. Ancak bu, tekrarlanan alandan kurtulmaya yardımcı oldu. Serileştirici sınıfında to_representation yöntemini geçersiz kılın: stackoverflow.com/questions/37985581/…
Aaron
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.