Django rest framework anidado objetos autorreferenciales

88

Tengo un modelo que se ve así:

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)

Me las arreglé para obtener una representación json plana de todas las categorías con serializador:

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

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

Ahora lo que quiero hacer es que la lista de subcategorías tenga una representación json en línea de las subcategorías en lugar de sus identificadores. ¿Cómo haría eso con django-rest-framework? Intenté encontrarlo en la documentación, pero parece incompleto.

Jacek Chmielewski
fuente

Respuestas:

70

En lugar de usar ManyRelatedField, use un serializador anidado como su 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')

Si desea tratar con campos anidados arbitrariamente, debe echar un vistazo a la parte de personalización de los campos predeterminados de los documentos. Actualmente no puede declarar directamente un serializador como un campo en sí mismo, pero puede usar estos métodos para anular qué campos se usan de forma predeterminada.

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 realidad, como ha notado, lo anterior no es del todo correcto. Esto es un truco, pero puede intentar agregar el campo después de que el serializador ya esté declarado.

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

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

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

Un mecanismo para declarar relaciones recursivas es algo que debe agregarse.


Editar : tenga en cuenta que ahora hay un paquete de terceros disponible que trata específicamente con este tipo de caso de uso. Consulte djangorestframework-recursive .

Tom Christie
fuente
3
Ok, esto funciona para la profundidad = 1. ¿Qué sucede si tengo más niveles en el árbol de objetos? ¿La categoría tiene una subcategoría que tiene una subcategoría? Quiero representar todo el árbol de profundidad arbitraria con objetos en línea. Usando su enfoque, no puedo definir el campo de subcategoría en SubCategorySerializer.
Jacek Chmielewski
Editado con más información sobre serializadores autorreferenciales.
Tom Christie
Ahora tengo KeyError at /api/category/ 'subcategories'. Por cierto, gracias por sus respuestas súper rápidas :)
Jacek Chmielewski
4
Para cualquier persona que vea esta pregunta por primera vez, descubrí que para cada nivel recursivo adicional, tenía que repetir la última línea en la segunda edición. Solución extraña, pero parece funcionar.
Jeremy Blalock
19
Solo me gustaría señalar que "base_fields" ya no funciona. Con DRF 3.1.0 "_declared_fields" es donde está la magia.
Travis Swientek
50

La solución de @ wjin funcionó muy bien para mí hasta que actualicé a Django REST framework 3.0.0, que desaprueba to_native . Aquí está mi solución DRF 3.0, que es una pequeña modificación.

Supongamos que tiene un modelo con un campo autorreferencial, por ejemplo, comentarios enhebrados en una propiedad llamada "respuestas". Tiene una representación de árbol de este hilo de comentarios y desea serializar el árbol

Primero, defina su clase RecursiveField reutilizable

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

Luego, para su serializador, use el RecursiveField para serializar el valor de "respuestas"

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

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

Fácil, y solo necesita 4 líneas de código para una solución reutilizable.

NOTA: Si su estructura de datos es más complicada que un árbol, como por ejemplo un gráfico acíclico dirigido (¡FANCY!), Entonces podría probar el paquete de @ wjin - vea su solución. Pero no he tenido ningún problema con esta solución para árboles basados ​​en MPTTModel.

Mark Chackerian
fuente
1
¿Qué hace la línea serializer = self.parent.parent .__ class __ (value, context = self.context)? ¿Es el método to_representation ()?
Mauricio
Esta línea es la parte más importante: permite que la representación del campo haga referencia al serializador correcto. En este ejemplo, creo que sería el CommentSerializer.
Mark Chackerian
1
Lo siento. No pude entender qué está haciendo este código. Lo ejecuté y funciona. Pero no tengo idea de cómo funciona realmente.
Mauricio
Intente poner algunas declaraciones impresas como print self.parent.parent.__class__yprint self.parent.parent
Mark Chackerian
La solución funciona pero la salida de conteo de mi serializador es incorrecta. Solo cuenta los nodos raíz. ¿Algunas ideas? Es lo mismo con djangorestframework-recursive.
Lucas Veiga
37

Otra opción que funciona 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
yprez
fuente
6
¿Por qué esta no es la respuesta aceptada? Funciona perfectamente.
Karthik RP
5
Esto funciona de manera muy simple, me resultó mucho más fácil lograr que esto funcionara que las otras soluciones publicadas.
Nick BL
Esta solución no necesita clases adicionales y es más fácil de entender que las parent.parent.__class__cosas. Me gusta más.
SergiyKolesnikov
27

Tarde para el juego aquí, pero aquí está mi solución. Digamos que estoy serializando un Blah, con varios hijos también del tipo Blah.

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

Usando este campo puedo serializar mis objetos definidos recursivamente que tienen muchos objetos secundarios

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

Escribí un campo recursivo para DRF3.0 y lo empaqueté para pip https://pypi.python.org/pypi/djangorestframework-recursive/

wjin
fuente
1
Funciona con la serialización de un modelo MPTTM. ¡Agradable!
Mark Chackerian
2
¿Todavía consigues que el niño se repita en la raíz, aunque? ¿Cómo puedo detener esto?
Prometheus
Lo siento @Sputnik, no entiendo lo que quieres decir. Lo que he dado aquí funciona para el caso en el que tiene una clase Blahy tiene un campo llamado child_blahsque consiste en una lista de Blahobjetos.
wjin
4
Esto funcionó muy bien hasta que actualicé a DRF 3.0, así que publiqué una variación 3.0.
Mark Chackerian
1
@ Falcon1 Puede filtrar conjunto de consultas y solo pasar nodos raíz en vistas como queryset=Class.objects.filter(level=0). Maneja el resto de las cosas por sí mismo.
chhantyal
13

Pude lograr este resultado usando un serializers.SerializerMethodField. No estoy seguro de si esta es la mejor manera, pero funcionó para mí:

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
fuente
1
Para mí, se redujo a elegir entre esta solución y la solución de yprez . Son más claras y sencillas que las soluciones publicadas anteriormente. La solución aquí ganó porque descubrí que es la mejor manera de resolver el problema presentado por el OP aquí y al mismo tiempo admitir esta solución para seleccionar dinámicamente los campos que se serializarán . La solución de Yprez provoca una recursividad infinita o requiere complicaciones adicionales para evitar la recursividad y seleccionar correctamente los campos.
Louis
9

Otra opción sería recurrir a la vista que serializa tu modelo. He aquí un ejemplo:

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
fuente
¡Esto es genial, tenía un árbol arbitrariamente profundo que necesitaba serializar y esto funcionó como un encanto!
Víðir Orri Reynisson
Respuesta buena y muy útil. Al obtener elementos secundarios en ModelSerializer, no puede especificar un conjunto de consultas para obtener elementos secundarios. En este caso, puedes hacerlo.
Efrin
8

Recientemente tuve el mismo problema y se me ocurrió una solución que parece funcionar hasta ahora, incluso para una profundidad arbitraria. La solución es una pequeña modificación de la 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()

Sin embargo, no estoy seguro de que pueda funcionar de manera confiable en cualquier situación ...

caipirginka
fuente
1
A partir de 2.3.8, no existe el método convert_object. Pero se puede hacer lo mismo anulando el método to_native.
abhaga
6

Esta es una adaptación de la solución caipirginka que funciona en drf 3.0.5 y 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')

Tenga en cuenta que el CategorySerializer en la sexta línea se llama con el objeto y el atributo many = True.

Wicho Valdeavellano
fuente
Increíble, esto funcionó para mí. Sin embargo, creo que if 'branches'debería cambiarse aif 'subcategories'
vabada
5

¡Pensé en unirme a la diversión!

A través de wjin y Mark Chackerian , creé una solución más general, que funciona para modelos directos en forma de árbol y estructuras de árbol que tienen un modelo transversal . No estoy seguro de si esto pertenece a su propia respuesta, pero pensé que podría ponerlo en alguna parte. Incluí una opción max_depth que evitará la recursividad infinita, en el nivel más profundo, los niños se representan como URLS (esa es la cláusula else final si prefiere que no sea una 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
fuente
Esta es una solución muy completa, sin embargo, vale la pena señalar que su elsecláusula hace ciertas suposiciones sobre la vista. Tuve que reemplazar el mío con return value.pkpara que devolviera las claves primarias en lugar de intentar revertir la búsqueda de la vista.
Soviut
4

Con el marco Django REST 3.3.1, necesitaba el siguiente código para agregar subcategorías a las categorías:

modelos.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
fuente
1

Esta solución es casi similar a las otras soluciones publicadas aquí, pero tiene una ligera diferencia en términos del problema de repetición de niños en el nivel raíz (si cree que es un problema). Para un ejemplo

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

y si tienes esta vista

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

Esto producirá el siguiente resultado,

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

Aquí parent categorytiene una child categoryy la representación json es exactamente lo que queremos que represente.

pero puede ver que hay una repetición del child categoryen el nivel raíz.

Como algunas personas preguntan en las secciones de comentarios de las respuestas publicadas anteriormente, ¿cómo podemos detener esta repetición secundaria en el nivel raíz? Simplemente filtre su conjunto de consultas con parent=None, como el siguiente

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

resolverá el problema.

NOTA: Es posible que esta respuesta no esté directamente relacionada con la pregunta, pero el problema está relacionado de alguna manera. Además, este enfoque de uso RecursiveSerializeres caro. Mejor si usa otras opciones que sean propensas al rendimiento.

Md. Tanvir Raihan
fuente
El conjunto de consultas con el filtro me causó un error. Pero esto ayudó a deshacerse del campo repetido. Anule el método to_representation en la clase de serializador: stackoverflow.com/questions/37985581/…
Aaron