Django Rest Framework verschachtelte selbstreferenzielle Objekte

86

Ich habe ein Modell, das so aussieht:

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)

Ich habe es geschafft, mit dem Serializer eine flache JSON-Darstellung aller Kategorien zu erhalten:

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

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

Was ich jetzt tun möchte, ist, dass die Liste der Unterkategorien eine Inline-JSON-Darstellung der Unterkategorien anstelle ihrer IDs enthält. Wie würde ich das mit dem Django-Rest-Framework machen? Ich habe versucht, es in der Dokumentation zu finden, aber es scheint unvollständig.

Jacek Chmielewski
quelle

Antworten:

70

Verwenden Sie anstelle von ManyRelatedField einen verschachtelten Serializer als Feld:

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

Wenn Sie sich mit willkürlich verschachtelten Feldern befassen möchten, sollten Sie sich den Abschnitt zum Anpassen der Standardfelder in den Dokumenten ansehen . Sie können einen Serializer derzeit nicht direkt als Feld für sich selbst deklarieren, aber Sie können diese Methoden verwenden, um zu überschreiben, welche Felder standardmäßig verwendet werden.

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

Wie Sie bereits bemerkt haben, ist das eigentlich nicht ganz richtig. Dies ist ein kleiner Hack, aber Sie können versuchen, das Feld hinzuzufügen, nachdem der Serializer bereits deklariert wurde.

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

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

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

Ein Mechanismus zum Deklarieren rekursiver Beziehungen muss hinzugefügt werden.


Bearbeiten : Beachten Sie, dass jetzt ein Paket von Drittanbietern verfügbar ist, das sich speziell mit dieser Art von Anwendungsfall befasst. Siehe djangorestframework-recursive .

Tom Christie
quelle
3
Ok, das funktioniert für Tiefe = 1. Was ist, wenn ich mehr Ebenen im Objektbaum habe - Kategorie hat Unterkategorie, die Unterkategorie hat? Ich möchte den gesamten Baum beliebiger Tiefe mit Inline-Objekten darstellen. Mit Ihrem Ansatz kann ich in SubCategorySerializer kein Unterkategoriefeld definieren.
Jacek Chmielewski
Bearbeitet mit weiteren Informationen zu selbstreferenziellen Serialisierern.
Tom Christie
Jetzt habe ich KeyError at /api/category/ 'subcategories'. Übrigens danke für deine superschnellen Antworten :)
Jacek Chmielewski
4
Für jeden, der diese Frage neu betrachtet, stellte ich fest, dass ich für jede zusätzliche rekursive Ebene die letzte Zeile in der zweiten Bearbeitung wiederholen musste. Seltsame Problemumgehung, scheint aber zu funktionieren.
Jeremy Blalock
19
Ich möchte nur darauf hinweisen, dass "base_fields" nicht mehr funktioniert. Mit DRF 3.1.0 ist "_declared_fields" der Ort, an dem die Magie liegt.
Travis Swientek
49

Die Lösung von @ wjin hat für mich hervorragend funktioniert, bis ich auf das Django REST Framework 3.0.0 aktualisiert habe, das to_native nicht mehr unterstützt . Hier ist meine DRF 3.0-Lösung, die eine geringfügige Modifikation darstellt.

Angenommen, Sie haben ein Modell mit einem selbstreferenziellen Feld, z. B. Kommentare mit Thread in einer Eigenschaft namens "Antworten". Sie haben eine Baumdarstellung dieses Kommentarthreads und möchten den Baum serialisieren

Definieren Sie zunächst Ihre wiederverwendbare RecursiveField-Klasse

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

Verwenden Sie dann für Ihren Serializer das RecursiveField, um den Wert von "Antworten" zu serialisieren.

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

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

Einfach peasy, und Sie benötigen nur 4 Codezeilen für eine wiederverwendbare Lösung.

HINWEIS: Wenn Ihre Datenstruktur komplizierter ist als ein Baum, z. B. ein gerichteter azyklischer Graph (FANCY!), Können Sie das Paket von @ wjin ausprobieren - sehen Sie sich seine Lösung an. Ich hatte jedoch keine Probleme mit dieser Lösung für MPTTModel-basierte Bäume.

Mark Chackerian
quelle
1
Was macht die Zeile serializer = self.parent.parent .__ class __ (value, context = self.context)? Ist es die Methode to_representation ()?
Mauricio
Diese Zeile ist der wichtigste Teil - sie ermöglicht es der Darstellung des Feldes, auf den richtigen Serializer zu verweisen. In diesem Beispiel wäre es meiner Meinung nach der CommentSerializer.
Mark Chackerian
1
Es tut mir Leid. Ich konnte nicht verstehen, was dieser Code tut. Ich habe es ausgeführt und es funktioniert. Aber ich habe keine Ahnung, wie es tatsächlich funktioniert.
Mauricio
Versuchen Sie, einige Druckaussagen wie print self.parent.parent.__class__undprint self.parent.parent
Mark Chackerian
Die Lösung funktioniert, aber die Zählausgabe meines Serializers ist falsch. Es werden nur Wurzelknoten gezählt. Irgendwelche Ideen? Das gleiche gilt für djangorestframework-recursive.
Lucas Veiga
29

Eine weitere Option, die mit Django REST Framework 3.3.2 funktioniert:

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
yprez
quelle
4
Warum ist dies nicht die akzeptierte Antwort? Funktioniert perfekt.
Karthik RP
2
Dies funktioniert sehr einfach. Ich hatte es viel einfacher, dies zum Laufen zu bringen als die anderen veröffentlichten Lösungen.
Nick BL
27

Spät zum Spiel hier, aber hier ist meine Lösung. Nehmen wir an, ich serialisiere ein Blah mit mehreren Kindern vom Typ Blah.

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

Mit diesem Feld kann ich meine rekursiv definierten Objekte mit vielen untergeordneten Objekten serialisieren

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

Ich habe ein rekursives Feld für DRF3.0 geschrieben und es für pip https://pypi.python.org/pypi/djangorestframework-recursive/ gepackt.

wjin
quelle
1
Funktioniert mit der Serialisierung eines MPTTModels. Nett!
Mark Chackerian
2
Sie bekommen das Kind immer noch an der Wurzel wiederholt tho? Wie kann ich das aufhalten?
Prometheus
Sorry @Sputnik Ich verstehe nicht was du meinst. Was ich hier gegeben habe, funktioniert für den Fall, dass Sie eine Klasse haben Blahund ein Feld namens aufgerufen hat, child_blahsdas aus einer Liste von BlahObjekten besteht.
wjin
4
Dies hat großartig funktioniert, bis ich auf DRF 3.0 aktualisiert habe, also habe ich eine 3.0-Variante veröffentlicht.
Mark Chackerian
1
@ Falcon1 Sie können Queryset filtern und Stammknoten nur in Ansichten wie übergeben queryset=Class.objects.filter(level=0). Es kümmert sich um den Rest der Dinge selbst.
Chhantyal
12

Dieses Ergebnis konnte ich mit a erzielen serializers.SerializerMethodField. Ich bin mir nicht sicher, ob dies der beste Weg ist, habe aber für mich gearbeitet:

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
jarussi
quelle
1
Für mich kam es auf die Wahl zwischen dieser Lösung und der Lösung von yprez an . Sie sind klarer und einfacher als die zuvor veröffentlichten Lösungen. Die Lösung hier gewonnen, weil ich fand , dass es der beste Weg, um das Problem durch die OP hier vorgestellt zu lösen und zugleich unterstützt diese Lösung für die dynamische Auswahl Felder serialisiert werden . Die Lösung von Yprez verursacht eine unendliche Rekursion oder erfordert zusätzliche Komplikationen, um die Rekursion zu vermeiden und Felder richtig auszuwählen.
Louis
9

Eine andere Möglichkeit wäre, in der Ansicht zu rekursieren, die Ihr Modell serialisiert. Hier ist ein Beispiel:

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)
Stefan Reinhard
quelle
Das ist großartig, ich hatte einen willkürlich tiefen Baum, den ich serialisieren musste, und das funktionierte wie ein Zauber!
Víðir Orri Reynisson
Gute und sehr nützliche Antwort. Wenn Sie untergeordnete Elemente in ModelSerializer abrufen, können Sie kein Abfrageset zum Abrufen untergeordneter Elemente angeben. In diesem Fall können Sie das tun.
Efrin
8

Ich hatte kürzlich das gleiche Problem und fand eine Lösung, die bis jetzt zu funktionieren scheint, selbst für willkürliche Tiefen. Die Lösung ist eine kleine Modifikation der von 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()

Ich bin mir nicht sicher, ob es in jeder Situation zuverlässig funktionieren kann ...

Caipirginka
quelle
1
Ab 2.3.8 gibt es keine convert_object-Methode. Das Gleiche kann jedoch durch Überschreiben der Methode to_native erreicht werden.
Abhaga
6

Dies ist eine Anpassung der Caipirginka-Lösung, die auf drf 3.0.5 und django 2.7.4 funktioniert:

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

Beachten Sie, dass der CategorySerializer in der 6. Zeile mit dem Objekt und dem Attribut many = True aufgerufen wird.

Wicho Valdeavellano
quelle
Erstaunlich, das hat bei mir funktioniert. Ich denke jedoch, dass das if 'branches'geändert werden sollte zuif 'subcategories'
vabada
5

Ich dachte, ich würde mitmachen!

Über wjin und Mark Chackerian habe ich eine allgemeinere Lösung erstellt, die für direkte baumartige Modelle und Baumstrukturen mit einem Durchgangsmodell funktioniert. Ich bin mir nicht sicher, ob dies zu seiner eigenen Antwort gehört, aber ich dachte, ich könnte es genauso gut irgendwo hinstellen. Ich habe eine Option max_depth eingefügt, die eine unendliche Rekursion verhindert. Auf der tiefsten Ebene werden Kinder als URLs dargestellt (dies ist die letzte else-Klausel, wenn Sie lieber keine URL möchten).

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'])
Will S.
quelle
Dies ist eine sehr gründliche Lösung. Es ist jedoch erwähnenswert, dass Ihre elseKlausel bestimmte Annahmen über die Ansicht enthält. Ich musste meine durch ersetzen, return value.pkdamit Primärschlüssel zurückgegeben wurden, anstatt zu versuchen, die Ansicht rückwärts nachzuschlagen.
Soviut
4

Mit Django REST Framework 3.3.1 benötigte ich den folgenden Code, um Unterkategorien zu Kategorien hinzuzufügen:

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')
AndraD
quelle
1

Diese Lösung ähnelt fast den anderen hier veröffentlichten Lösungen, weist jedoch einen geringfügigen Unterschied in Bezug auf das Problem der Wiederholung von Kindern auf der Stammebene auf (wenn Sie der Meinung sind, dass dies ein Problem ist). Zum Beispiel

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

und wenn Sie diese Ansicht haben

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

Dies führt zu folgendem Ergebnis:

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

Hier parent categoryhat child categorydie a- und die json-Darstellung genau das, was wir wollen, dass sie dargestellt wird.

aber Sie können sehen, dass es eine Wiederholung der child categoryauf der Wurzelebene gibt.

Da einige Leute in den Kommentaren der oben veröffentlichten Antworten fragen, wie wir diese untergeordnete Wiederholung auf der Stammebene stoppen können , filtern Sie einfach Ihr Abfrageset mit parent=Nonewie folgt

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

es wird das Problem lösen.

HINWEIS: Diese Antwort steht möglicherweise nicht in direktem Zusammenhang mit der Frage, aber das Problem hängt irgendwie zusammen. Auch dieser Verwendungsansatz RecursiveSerializerist teuer. Besser, wenn Sie andere Optionen verwenden, die leistungsanfällig sind.

Md. Tanvir Raihan
quelle
Das Abfrageset mit dem Filter hat bei mir einen Fehler verursacht. Aber dies half, das wiederholte Feld loszuwerden. Überschreiben Sie die to_representation-Methode in der Serializer-Klasse: stackoverflow.com/questions/37985581/…
Aaron