Django rest framework annidato oggetti autoreferenziali


90

Ho un modello che assomiglia a questo:

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)

Sono riuscito a ottenere una rappresentazione json piatta di tutte le categorie con serializzatore:

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

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

Quello che voglio fare è che l'elenco delle sottocategorie abbia una rappresentazione json in linea delle sottocategorie invece dei loro ID. Come lo farei con django-rest-framework? Ho provato a trovarlo nella documentazione, ma sembra incompleto.

Risposte:


70

Invece di usare ManyRelatedField, usa un serializzatore annidato come campo:

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

Se vuoi gestire i campi nidificati arbitrariamente dovresti dare un'occhiata alla personalizzazione dei campi predefiniti parte della documentazione. Al momento non è possibile dichiarare direttamente un serializzatore come campo su se stesso, ma è possibile utilizzare questi metodi per sovrascrivere i campi utilizzati per impostazione predefinita.

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

In realtà, come hai notato, quanto sopra non è del tutto corretto. Questo è un po 'un trucco, ma potresti provare ad aggiungere il campo dopo che il serializzatore è già stato dichiarato.

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

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

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

Un meccanismo di dichiarazione di relazioni ricorsive è qualcosa che deve essere aggiunto.


Modifica : nota che ora è disponibile un pacchetto di terze parti che si occupa specificamente di questo tipo di caso d'uso. Vedi djangorestframework-recursive .


4
Ok, questo funziona per la profondità = 1. Cosa succede se ho più livelli nell'albero degli oggetti: la categoria ha una sottocategoria che ha una sottocategoria? Voglio rappresentare l'intero albero di profondità arbitraria con oggetti in linea. Utilizzando il tuo approccio, non riesco a definire il campo della sottocategoria in SubCategorySerializer.
Jacek Chmielewski

Modificato con ulteriori informazioni sui serializzatori autoreferenziali.
Tom Christie

4
Per chiunque visualizzi questa domanda, ho scoperto che per ogni livello ricorsivo extra, dovevo ripetere l'ultima riga nella seconda modifica. Strana soluzione alternativa, ma sembra funzionare.
Jeremy Blalock

1
@ TomChristie Hai ancora il bambino ripetuto alla radice, però? Come posso fermarlo?
Prometeo dal

20
Vorrei solo sottolineare che "base_fields" non funziona più. Con DRF 3.1.0 "_declared_fields" è dove sta la magia.
Travis Swientek

51

La soluzione di @ wjin stava funzionando alla grande per me fino a quando non ho aggiornato a Django REST framework 3.0.0, che rende obsoleto to_native . Ecco la mia soluzione DRF 3.0, che è una leggera modifica.

Supponiamo di avere un modello con un campo autoreferenziale, ad esempio commenti in thread in una proprietà chiamata "risposte". Si dispone di una rappresentazione ad albero di questo thread di commenti e si desidera serializzare l'albero

Innanzitutto, definisci la tua classe RecursiveField riutilizzabile

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

Quindi, per il serializzatore, utilizza il RecursiveField per serializzare il valore di "replies"

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

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

Facile, e hai solo bisogno di 4 righe di codice per una soluzione riutilizzabile.

NOTA: Se la tua struttura dati è più complicata di un albero, come ad esempio un grafico aciclico diretto (FANCY!), Allora potresti provare il pacchetto @ wjin - guarda la sua soluzione. Ma non ho avuto problemi con questa soluzione per alberi basati su MPTTModel.


1
Cosa fa la riga serializer = self.parent.parent .__ class __ (value, context = self.context). È il metodo to_representation ()?
Mauricio

Questa riga è la parte più importante: consente la rappresentazione del campo per fare riferimento al serializzatore corretto. In questo esempio, credo che sarebbe CommentSerializer.
Mark Chackerian

1
Mi dispiace. Non sono riuscito a capire cosa stia facendo questo codice. L'ho eseguito e funziona. Ma non ho idea di come funzioni effettivamente.
Mauricio

Prova a inserire alcune dichiarazioni di stampa come print self.parent.parent.__class__eprint self.parent.parent
Mark Chackerian,

La soluzione funziona ma l'output del conteggio del serializzatore è sbagliato. Conta solo i nodi root. Qualche idea? È lo stesso con djangorestframework-recursive.
Lucas Veiga

39

Un'altra opzione che funziona con 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
Perché questa non è la risposta accettata? Funziona perfettamente.
Karthik RP

5
Funziona in modo molto semplice, è stato molto più facile farlo funzionare rispetto alle altre soluzioni pubblicate.
Nick BL

Questa soluzione non ha bisogno di classi extra ed è più facile da capire del parent.parent.__class__materiale. Mi piace di più.
SergiyKolesnikov

In python 3, può essere così:fields = super().get_fields()
Elinaldo Monteiro

Questa potrebbe non essere un'opzione se desideri utilizzare l'endpoint OPTIONS delle tue viste, si blocca in un ciclo infinito se ho usato questo approccio. La soluzione RecursiveField ha funzionato per me ed è anche riutilizzabile.
Prasad Pilla

30

In ritardo per la partita qui, ma ecco la mia soluzione. Diciamo che sto serializzando un Blah, con più figli anche di tipo Blah.

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

Usando questo campo posso serializzare i miei oggetti definiti in modo ricorsivo che hanno molti oggetti figlio

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

Ho scritto un campo ricorsivo per DRF3.0 e l'ho impacchettato per pip https://pypi.python.org/pypi/djangorestframework-recursive/


1
Funziona con la serializzazione di un MPTTModel. Bello!
Mark Chackerian

2
Continui a far ripetere il bambino alla radice? Come posso fermarlo?
Prometeo dal

Scusa @Sputnik, non capisco cosa intendi. Quello che ho dato qui funziona per il caso in cui hai una classe Blahe ha un campo chiamato child_blahsche consiste in un elenco di Blahoggetti.
wjin

4
Funzionava benissimo finché non sono passato a DRF 3.0, quindi ho pubblicato una variazione 3.0.
Mark Chackerian

1
@ Falcon1 È possibile filtrare il set di query e passare i nodi root solo in viste come queryset=Class.objects.filter(level=0). Gestisce il resto delle cose da solo.
chhantyal

15

Sono stato in grado di ottenere questo risultato utilizzando un file serializers.SerializerMethodField. Non sono sicuro che questo sia il modo migliore, ma ha funzionato per me:

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
Per me si è trattato di una scelta tra questa soluzione e quella di yprez . Sono entrambi più chiari e più semplici delle soluzioni pubblicate in precedenza. La soluzione qui ha vinto perché ho scoperto che è il modo migliore per risolvere il problema presentato dall'OP qui e allo stesso tempo supportare questa soluzione per la selezione dinamica dei campi da serializzare . La soluzione di Yprez causa una ricorsione infinita o richiede ulteriori complicazioni per evitare la ricorsione e selezionare correttamente i campi.
Louis

9

Un'altra opzione potrebbe essere quella di ricorrere nella vista che serializza il tuo modello. Ecco un esempio:

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)

È fantastico, avevo un albero arbitrariamente profondo che dovevo serializzare e ha funzionato a meraviglia!
Víðir Orri Reynisson

Risposta buona e molto utile. Quando si ottengono figli su ModelSerializer non è possibile specificare un set di query per ottenere elementi figlio. In questo caso puoi farlo.
Efrin

8

Recentemente ho avuto lo stesso problema e ho trovato una soluzione che sembra funzionare finora, anche per profondità arbitrarie. La soluzione è una piccola modifica di quella di 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()

Non sono sicuro che possa funzionare in modo affidabile in qualsiasi situazione, però ...


1
A partire dalla 2.3.8, non esiste alcun metodo convert_object. Ma la stessa cosa può essere fatta sovrascrivendo il metodo to_native.
abhaga

6

Questo è un adattamento della soluzione caipirginka che funziona su drf 3.0.5 e 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')

Notare che il CategorySerializer nella sesta riga viene chiamato con l'oggetto e l'attributo many = True.


Fantastico, questo ha funzionato per me. Tuttavia, penso che if 'branches'dovrebbe essere cambiato inif 'subcategories'
vabada

6

Ho pensato di unirmi al divertimento!

Via wjin e Mark Chackerian ho creato una soluzione più generale, che funziona per modelli ad albero diretti e strutture ad albero che hanno un modello passante. Non sono sicuro che questo appartenga alla sua risposta, ma ho pensato che avrei potuto anche metterlo da qualche parte. Ho incluso un'opzione max_depth che impedirà la ricorsione infinita, al livello più profondo i bambini sono rappresentati come URL (questa è la clausola else finale se preferisci che non fosse un 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
Questa è una soluzione molto completa, tuttavia, vale la pena notare che la tua elseclausola fa alcune ipotesi sulla vista. Ho dovuto sostituire il mio con return value.pkcosì ha restituito le chiavi primarie invece di cercare di invertire la ricerca nella vista.
Soviut

4

Con Django REST framework 3.3.1, avevo bisogno del seguente codice per aggiungere sottocategorie alle categorie:

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

Questa soluzione è quasi simile alle altre soluzioni pubblicate qui, ma presenta una leggera differenza in termini di problema di ripetizione infantile a livello di radice (se pensi che sia un problema). Per un esempio

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

e se hai questa visione

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

Questo produrrà il seguente risultato,

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

Qui il file parent category ha un filechild category e la rappresentazione json è esattamente ciò che vogliamo che rappresenti.

ma puoi vedere che c'è una ripetizione di child category a livello di radice.

Poiché alcune persone chiedono nelle sezioni dei commenti delle risposte postate sopra che come possiamo fermare questa ripetizione del bambino a livello di root , filtra semplicemente il tuo set di query con parent=None, come segue

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

risolverà il problema.

NOTA: questa risposta potrebbe non essere direttamente correlata alla domanda, ma il problema è in qualche modo correlato. Anche questo approccio di utilizzo RecursiveSerializerè costoso. Meglio se usi altre opzioni che sono soggette a prestazioni.


Il set di query con il filtro ha causato un errore per me. Ma questo ha aiutato a sbarazzarsi del campo ripetuto. Sostituisci il metodo to_representation nella classe serializzatore: stackoverflow.com/questions/37985581/…
Aaron
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.