Django Rest Framework Objets auto-référentiels imbriqués


88

J'ai un modèle qui ressemble à ceci:

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)

J'ai réussi à obtenir une représentation json plate de toutes les catégories avec le sérialiseur:

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

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

Maintenant, ce que je veux faire, c'est que la liste des sous-catégories ait une représentation json en ligne des sous-catégories au lieu de leurs identifiants. Comment ferais-je cela avec django-rest-framework? J'ai essayé de le trouver dans la documentation, mais cela semble incomplet.

Réponses:


70

Au lieu d'utiliser ManyRelatedField, utilisez un sérialiseur imbriqué comme champ:

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

Si vous souhaitez traiter des champs imbriqués de manière arbitraire, vous devriez jeter un œil à la personnalisation des champs par défaut dans la documentation. Vous ne pouvez actuellement pas déclarer directement un sérialiseur en tant que champ sur lui-même, mais vous pouvez utiliser ces méthodes pour remplacer les champs utilisés par défaut.

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

En fait, comme vous l'avez noté, ce qui précède n'est pas tout à fait exact. C'est un peu un hack, mais vous pouvez essayer d'ajouter le champ après que le sérialiseur soit déjà déclaré.

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

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

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

Un mécanisme de déclaration de relations récursives est quelque chose qui doit être ajouté.


Edit : Notez qu'il existe désormais un package tiers disponible qui traite spécifiquement ce type de cas d'utilisation. Voir djangorestframework-recursive .


3
Ok, cela fonctionne pour la profondeur = 1. Que faire si j'ai plus de niveaux dans l'arborescence d'objets - la catégorie a une sous-catégorie qui a une sous-catégorie? Je veux représenter l'arbre entier de profondeur arbitraire avec des objets en ligne. En utilisant votre approche, je ne peux pas définir le champ de sous-catégorie dans SubCategorySerializer.
Jacek Chmielewski

Édité avec plus d'informations sur les sérialiseurs auto-référentiels.
Tom Christie

Maintenant je l'ai KeyError at /api/category/ 'subcategories'. Btw merci pour vos réponses ultra-rapides :)
Jacek Chmielewski

4
Pour toute nouvelle consultation de cette question, j'ai trouvé que pour chaque niveau récursif supplémentaire, je devais répéter la dernière ligne de la deuxième édition. Solution de contournement étrange, mais semble fonctionner.
Jeremy Blalock

19
Je voudrais juste souligner que "base_fields" ne fonctionne plus. Avec DRF 3.1.0 "_declared_fields" est la magie.
Travis Swientek

50

La solution de @ wjin fonctionnait très bien pour moi jusqu'à ce que je passe à Django REST Framework 3.0.0, qui désapprouve to_native . Voici ma solution DRF 3.0, qui est une légère modification.

Supposons que vous ayez un modèle avec un champ auto-référentiel, par exemple des commentaires filetés dans une propriété appelée «réponses». Vous avez une représentation arborescente de ce fil de commentaire et vous souhaitez sérialiser l'arborescence

Tout d'abord, définissez votre classe RecursiveField réutilisable

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

Ensuite, pour votre sérialiseur, utilisez le RecursiveField pour sérialiser la valeur de «réponses»

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

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

Facile, et vous n'avez besoin que de 4 lignes de code pour une solution réutilisable.

REMARQUE: Si votre structure de données est plus compliquée qu'un arbre, comme par exemple un graphe acyclique dirigé (FANCY!), Alors vous pouvez essayer le package de @ wjin - voir sa solution. Mais je n'ai eu aucun problème avec cette solution pour les arbres basés sur MPTTModel.


1
Que fait la ligne serializer = self.parent.parent .__ class __ (value, context = self.context). Est-ce la méthode to_representation ()?
Mauricio

Cette ligne est la partie la plus importante - elle permet à la représentation du champ de référencer le sérialiseur correct. Dans cet exemple, je pense que ce serait le CommentSerializer.
Mark Chackerian

1
Je suis désolé. Je ne pouvais pas comprendre ce que faisait ce code. Je l'ai couru et ça marche. Mais je n'ai aucune idée de comment cela fonctionne réellement.
Mauricio

Essayez de mettre dans quelques déclarations imprimées comme print self.parent.parent.__class__etprint self.parent.parent
Mark Chackerian

La solution fonctionne mais la sortie de comptage de mon sérialiseur est incorrecte. Il ne compte que les nœuds racine. Des idées? C'est la même chose avec djangorestframework-recursive.
Lucas Veiga

37

Une autre option qui fonctionne avec 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
Pourquoi n'est-ce pas la réponse acceptée? Fonctionne parfaitement.
Karthik RP

5
Cela fonctionne très simplement, j'ai eu beaucoup plus de facilité à faire fonctionner cela que les autres solutions publiées.
Nick BL

Cette solution n'a pas besoin de classes supplémentaires et est plus facile à comprendre que les parent.parent.__class__choses. Je l'aime le plus.
SergiyKolesnikov

27

Tard dans le jeu ici, mais voici ma solution. Disons que je sérialise un Blah, avec plusieurs enfants également de type Blah.

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

En utilisant ce champ, je peux sérialiser mes objets définis de manière récursive qui ont de nombreux objets enfants

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

J'ai écrit un champ récursif pour DRF3.0 et l'ai empaqueté pour pip https://pypi.python.org/pypi/djangorestframework-recursive/


1
Fonctionne avec la sérialisation d'un MPTTModel. Agréable!
Mark Chackerian

2
Vous obtenez toujours l'enfant répété à la racine tho? Comment puis-je arrêter ça?
Prometheus

Désolé @Sputnik, je ne comprends pas ce que vous voulez dire. Ce que j'ai donné ici fonctionne pour le cas où vous avez une classe Blahet qu'elle a un champ appelé child_blahsqui consiste en une liste d' Blahobjets.
wjin

4
Cela fonctionnait très bien jusqu'à ce que je passe à DRF 3.0, j'ai donc publié une variante 3.0.
Mark Chackerian

1
@ Falcon1 Vous pouvez filtrer l'ensemble de requêtes et ne transmettre que les nœuds racine dans des vues telles que queryset=Class.objects.filter(level=0). Il gère le reste des choses lui-même.
chhantyal

13

J'ai pu obtenir ce résultat en utilisant un serializers.SerializerMethodField. Je ne sais pas si c'est le meilleur moyen, mais cela a fonctionné pour moi:

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
Pour moi, cela se résumait à un choix entre cette solution et la solution d'Yprez . Elles sont à la fois plus claires et plus simples que les solutions publiées précédemment. La solution ici l'a emporté car j'ai trouvé que c'était le meilleur moyen de résoudre le problème présenté par l'OP ici et en même temps de prendre en charge cette solution pour sélectionner dynamiquement les champs à sérialiser . La solution d'Yprez provoque une récursion infinie ou nécessite des complications supplémentaires pour éviter la récursivité et sélectionner correctement les champs.
Louis

9

Une autre option serait de récurer dans la vue qui sérialise votre modèle. Voici un exemple:

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)

C'est génial, j'avais un arbre arbitrairement profond que j'avais besoin de sérialiser et cela a fonctionné comme un charme!
Víðir Orri Reynisson

Bonne réponse très utile. Lorsque vous obtenez des enfants sur ModelSerializer, vous ne pouvez pas spécifier un ensemble de requêtes pour obtenir des éléments enfants. Dans ce cas, vous pouvez le faire.
Efrin

8

J'ai récemment eu le même problème et j'ai trouvé une solution qui semble fonctionner jusqu'à présent, même pour une profondeur arbitraire. La solution est une petite modification de celle de 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()

Je ne suis pas sûr que cela puisse fonctionner de manière fiable dans n'importe quelle situation, cependant ...


1
Depuis 2.3.8, il n'y a pas de méthode convert_object. Mais la même chose peut être faite en remplaçant la méthode to_native.
abhaga

6

Ceci est une adaptation de la solution caipirginka qui fonctionne sur drf 3.0.5 et 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')

Notez que le CategorySerializer de la 6ème ligne est appelé avec l'objet et l'attribut many = True.


Incroyable, cela a fonctionné pour moi. Cependant, je pense que le if 'branches'devrait être changé enif 'subcategories'
vabada

5

J'ai pensé que je participerais à l'amusement!

Via wjin et Mark Chackerian, j'ai créé une solution plus générale, qui fonctionne pour les modèles d'arbres directs et les structures arborescentes qui ont un modèle traversant. Je ne sais pas si cela appartient à sa propre réponse, mais j'ai pensé que je pourrais aussi bien le mettre quelque part. J'ai inclus une option max_depth qui empêchera la récursivité infinie, au niveau le plus profond, les enfants sont représentés sous forme d'URL (c'est la dernière clause else si vous préférez que ce ne soit pas une 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'])

Il s'agit d'une solution très complète, cependant, il convient de noter que votre elseclause fait certaines hypothèses sur la vue. J'ai dû remplacer le mien par return value.pkdonc il a renvoyé les clés primaires au lieu d'essayer d'inverser la vue.
Soviut

4

Avec le framework Django REST 3.3.1, j'avais besoin du code suivant pour ajouter des sous-catégories aux catégories:

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

Cette solution est presque similaire aux autres solutions publiées ici mais présente une légère différence en termes de problème de répétition des enfants au niveau de la racine (si vous pensez que c'est un problème). À titre d'exemple

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

et si vous avez cette vue

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

Cela produira le résultat suivant,

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

Ici, la représentation parent categorya child categoryet la représentation json est exactement ce que nous voulons qu'elle représente.

mais vous pouvez voir qu'il y a une répétition du au child categoryniveau de la racine.

Comme certaines personnes demandent dans les sections de commentaires des réponses publiées ci-dessus, comment pouvons-nous arrêter cette répétition enfant au niveau de la racine , il suffit de filtrer votre jeu de requête avec parent=None, comme suit

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

cela résoudra le problème.

REMARQUE: Cette réponse n'est peut-être pas directement liée à la question, mais le problème est en quelque sorte lié. Cette approche d'utilisation RecursiveSerializerest également coûteuse. Mieux si vous utilisez d'autres options qui sont sujettes aux performances.


Le jeu de requêtes avec le filtre a provoqué une erreur pour moi. Mais cela a aidé à se débarrasser du champ répété. Remplacez la méthode to_representation dans la classe du sérialiseur: stackoverflow.com/questions/37985581/…
Aaron
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.