web-dev-qa-db-fra.com

Django rest framework nested object self-referential objects

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 pourrais-je faire cela avec Django-rest-framework? J'ai essayé de le trouver dans la documentation, mais il semble incomplet.

70
Jacek Chmielewski

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 arbitrairement, vous devriez jeter un œil à la partie personnalisation des champs par défaut des documents. Vous ne pouvez pas actuellement 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 correct. C'est un peu un hack, mais vous pouvez essayer d'ajouter le champ après que le sérialiseur est 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 des relations récursives doit être ajouté.


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

61
Tom Christie

La solution de @ wjin fonctionnait très bien pour moi jusqu'à ce que je passe à Django REST framework 3.0.0, qui déprécie to_native. Voici mon 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 des "réponses"

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

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

Peasy 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 disons un graphique acyclique dirigé (FANCY!), Alors vous pouvez essayer le paquet de @ wjin - voir sa solution. Mais je n'ai eu aucun problème avec cette solution pour les arbres basés sur MPTTModel.

42
Mark Chackerian

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/

24
wjin

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
19
yprez

Une autre option serait de reprendre 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)
9
Stefan Reinhard

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 toute situation, cependant ...

8
caipirginka

J'ai pu obtenir ce résultat en utilisant un serializers.SerializerMethodField. Je ne sais pas si c'est la meilleure façon, 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
8
jarussi

Il s'agit d'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 en 6ème ligne est appelé avec l'objet et l'attribut many = True.

6

Je pensais participer au plaisir!

Via wjin et Mark Chackerian j'ai créé une solution plus générale, qui fonctionne pour les modèles arborescents 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écursion infinie, au niveau le plus profond, les enfants sont représentés comme des 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'])
5
Will S

Avec Django REST framework 3.3.1, j'avais besoin du code suivant pour obtenir des sous-catégories ajoutées 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')
4
AndraD