Objetos auto-referenciais aninhados do framework Django rest

90

Tenho um modelo parecido com este:

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)

Consegui uma representação json plana de todas as categorias com o serializador:

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

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

Agora o que eu quero fazer é que a lista de subcategorias tenha uma representação json embutida das subcategorias em vez de seus ids. Como eu faria isso com o django-rest-framework? Tentei encontrar na documentação, mas parece incompleto.

Jacek Chmielewski
fonte

Respostas:

70

Em vez de usar ManyRelatedField, use um serializador aninhado como seu 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 você deseja lidar com campos aninhados arbitrariamente, deve dar uma olhada na parte de personalização dos campos padrão dos documentos. Atualmente, você não pode declarar diretamente um serializador como um campo em si mesmo, mas pode usar esses métodos para substituir quais campos são usados ​​por padrão.

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

Na verdade, como você observou, o que está acima não está certo. Isso é meio que um hack, mas você pode tentar adicionar o campo depois que o serializador já estiver declarado.

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

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

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

Um mecanismo de declaração de relacionamentos recursivos é algo que precisa ser adicionado.


Editar : Observe que agora existe um pacote de terceiros disponível que lida especificamente com esse tipo de caso de uso. Consulte djangorestframework-recursive .

Tom Christie
fonte
4
Ok, isso funciona para profundidade = 1. E se eu tiver mais níveis na árvore de objetos - categoria tem subcategoria que tem subcategoria? Quero representar toda a árvore de profundidade arbitrária com objetos embutidos. Usando sua abordagem, não posso definir o campo de subcategoria em SubCategorySerializer.
Jacek Chmielewski
Editado com mais informações sobre serializadores autorreferenciais.
Tom Christie
4
Para quem está começando a ver essa pergunta, descobri que, para cada nível recursivo extra, tive que repetir a última linha da segunda edição. Solução alternativa estranha, mas parece funcionar.
Jeremy Blalock,
1
@TomChristie Você ainda consegue repetir a criança na raiz? Como posso parar isso?
Prometheus,
20
Eu gostaria apenas de apontar, "base_fields" não funciona mais. Com DRF 3.1.0 "_declared_fields" é onde está a mágica.
Travis Swientek
51

A solução de @wjin estava funcionando muito bem para mim até que atualizei para o Django REST framework 3.0.0, que se tornou obsoleto para_native . Aqui está minha solução DRF 3.0, que é uma pequena modificação.

Digamos que você tenha um modelo com um campo autorreferencial, por exemplo, comentários encadeados em uma propriedade chamada "respostas". Você tem uma representação em árvore deste tópico de comentários e deseja serializar a árvore

Primeiro, defina sua classe reutilizável RecursiveField

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

Então, para o seu serializador, use o RecursiveField para serializar o valor de "respostas"

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

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

Fácil de usar, e você só precisa de 4 linhas de código para uma solução reutilizável.

NOTA: Se sua estrutura de dados é mais complicada do que uma árvore, como digamos um gráfico acíclico direcionado (FANCY!), Então você pode tentar o pacote de @wjin - veja a solução dele. Mas eu não tive nenhum problema com esta solução para árvores baseadas em MPTTModel.

Mark Chackerian
fonte
1
O que a linha serializer = self.parent.parent .__ class __ (value, context = self.context) faz. É o método to_representation ()?
Mauricio
Esta linha é a parte mais importante - permite a representação do campo para referenciar o serializador correto. Neste exemplo, acredito que seria o CommentSerializer.
Mark Chackerian
1
Eu sinto Muito. Não consigo entender o que esse código está fazendo. Eu executei e funciona. Mas não tenho ideia de como isso realmente funciona.
Mauricio
Tente colocar algumas declarações impressas como print self.parent.parent.__class__eprint self.parent.parent
Mark Chackerian
A solução funciona, mas a saída de contagem do meu serializador está errada. Ele conta apenas os nós raiz. Alguma ideia? É o mesmo com djangorestframework-recursive.
Lucas Veiga
39

Outra opção que funciona com 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
fonte
6
Por que essa não é a resposta aceita? Funciona perfeitamente.
Karthik RP
5
Isso funciona de forma muito simples, foi muito mais fácil fazer isso funcionar do que as outras soluções postadas.
Nick BL
Esta solução não precisa de classes extras e é mais fácil de entender do que as parent.parent.__class__outras coisas. Eu gosto mais disso
SergiyKolesnikov
Em python 3, pode ser assim:fields = super().get_fields()
Elinaldo Monteiro
Isso pode não ser uma opção se você quiser usar o endpoint OPTIONS de suas visualizações, ele fica preso em algum loop infinito se eu usar essa abordagem. A solução RecursiveField funcionou para mim e também pode ser reutilizada.
Prasad Pilla
30

Atrasado para o jogo aqui, mas aqui está minha solução. Digamos que eu esteja serializando um Blah, com vários filhos também do tipo Blah.

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

Usando este campo, posso serializar meus objetos definidos recursivamente que têm muitos objetos-filho

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

Eu escrevi um campo recursivo para DRF3.0 e empacotei para pip https://pypi.python.org/pypi/djangorestframework-recursive/

wjin
fonte
1
Funciona com a serialização de um MPTTModel. Agradável!
Mark Chackerian
2
Você ainda consegue repetir a criança na raiz? Como posso parar isso?
Prometheus,
Desculpe @Sputnik, não entendi o que você quis dizer. O que eu dei aqui funciona para o caso em que você tem uma classe Blahe ela tem um campo chamado child_blahsque consiste em uma lista de Blahobjetos.
termina em
4
Isso estava funcionando muito bem até que atualizei para o DRF 3.0, então postei uma variação 3.0.
Mark Chackerian
1
@ Falcon1 Você pode filtrar queryset e apenas passar nós raiz em visualizações como queryset=Class.objects.filter(level=0). Ele lida com o resto das coisas sozinho.
chhantyal de
15

Consegui atingir esse resultado usando um serializers.SerializerMethodField. Não tenho certeza se esta é a melhor maneira, mas funcionou para mim:

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
fonte
1
Para mim, tudo se resumiu a uma escolha entre esta solução e a solução de yprez . Eles são mais claros e simples do que as soluções postadas anteriormente. A solução aqui venceu porque achei que é a melhor forma de resolver o problema apresentado pelo OP aqui e ao mesmo tempo suportar essa solução para selecionar campos dinamicamente a serem serializados . A solução de Yprez causa uma recursão infinita ou requer complicações adicionais para evitar a recursão e selecionar campos corretamente.
Louis
9

Outra opção seria recursar na visualização que serializa seu modelo. Aqui está um exemplo:

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
fonte
Isso é ótimo, eu tinha uma árvore arbitrariamente profunda que precisava serializar e funcionou perfeitamente!
Víðir Orri Reynisson
Resposta boa e muito útil. Ao obter os filhos no ModelSerializer, você não pode especificar um queryset para obter os elementos filhos. Nesse caso, você pode fazer isso.
Efrin
8

Recentemente, tive o mesmo problema e descobri uma solução que parece funcionar até agora, mesmo em profundidades arbitrárias. A solução é uma pequena modificação da 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()

Não tenho certeza se pode funcionar de forma confiável em qualquer situação, no entanto ...

caipirginka
fonte
1
A partir de 2.3.8, não há método convert_object. Mas a mesma coisa pode ser feita substituindo o método to_native.
abhaga
6

Esta é uma adaptação da solução caipirginka que funciona no 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')

Observe que o CategorySerializer na 6ª linha é chamado com o objeto e o atributo many = True.

Wicho Valdeavellano
fonte
Incrível, isso funcionou para mim. No entanto, acho que if 'branches'deve ser alterado paraif 'subcategories'
vabada
6

Pensei em me juntar à diversão!

Via wjin e Mark Chackerian eu criei uma solução mais geral, que funciona para modelos diretos em forma de árvore e estruturas de árvore que têm um modelo direto. Não tenho certeza se isso pertence à sua própria resposta, mas achei melhor colocá-lo em algum lugar. Incluí uma opção max_depth que impedirá a recursão infinita, no nível mais profundo, os filhos são representados como URLS (essa é a cláusula else final, se você preferir que não seja um 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
fonte
1
Esta é uma solução muito completa, entretanto, é importante notar que sua elsecláusula faz certas suposições sobre a exibição. Tive que substituir o meu por return value.pkpara que ele retornasse as chaves primárias em vez de tentar reverter a visualização da visualização.
Soviut
4

Com o Django REST framework 3.3.1, eu precisava do seguinte código para obter subcategorias adicionadas às categorias:

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
fonte
2

Esta solução é quase semelhante às outras soluções postadas aqui, mas tem uma pequena diferença em termos de problema de repetição de criança no nível raiz (se você acha que é um problema). Por exemplo

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 você tem essa visão

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

Isso produzirá o seguinte 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": []
}
]

Aqui, o parent categorytem child categoryae a representação json é exatamente o que queremos que seja representado.

mas você pode ver que há uma repetição do child categoryno nível raiz.

Como algumas pessoas estão perguntando nas seções de comentários das respostas postadas acima, como podemos parar essa repetição infantil no nível raiz , basta filtrar seu queryset com parent=None, como a seguir

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

vai resolver o problema.

NOTA: Esta resposta pode não estar diretamente relacionada à pergunta, mas o problema está de alguma forma relacionado. Além disso, essa abordagem de uso RecursiveSerializeré cara. Melhor se você usar outras opções que são propensas ao desempenho.

Md. Tanvir Raihan
fonte
O queryset com o filtro causou um erro para mim. Mas isso ajudou a se livrar do campo repetido. Substitua o método to_representation na classe do serializador: stackoverflow.com/questions/37985581/…
Aaron