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.

Jacek Chmielewski
la source

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 .

Tom Christie
la source
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.

Mark Chackerian
la source
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
yprez
la source
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/

wjin
la source
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
Jarussi
la source
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)
Stefan Reinhard
la source
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 ...

caipirginka
la source
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.

Wicho Valdeavellano
la source
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'])
Will S
la source
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')
AndraD
la source
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.

Md. Tanvir Raihan
la source
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