Zagnieżdżone obiekty odwołujące się do siebie we frameworku Django

88

Mam model, który wygląda tak:

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)

Udało mi się uzyskać płaską reprezentację json wszystkich kategorii z serializatorem:

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

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

Teraz chcę, aby lista podkategorii miała wbudowaną reprezentację podkategorii w formacie JSON zamiast ich identyfikatorów. Jak miałbym to zrobić z django-rest-framework? Próbowałem znaleźć to w dokumentacji, ale wydaje się niekompletne.

Jacek Chmielewski
źródło

Odpowiedzi:

70

Zamiast używać ManyRelatedField, użyj zagnieżdżonego serializatora jako pola:

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

Jeśli chcesz poradzić sobie z arbitralnie zagnieżdżonymi polami, powinieneś przyjrzeć się dostosowywaniu domyślnych pól w dokumentacji. Obecnie nie można bezpośrednio zadeklarować serializatora jako samego pola, ale można użyć tych metod, aby przesłonić, które pola są używane domyślnie.

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

Właściwie, jak zauważyłeś, powyższe nie jest do końca poprawne. To trochę hack, ale możesz spróbować dodać pole po zadeklarowaniu serializatora.

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

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

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

Należy dodać mechanizm deklarowania relacji rekurencyjnych.


Edycja : Zwróć uwagę, że jest teraz dostępny pakiet innej firmy, który zajmuje się konkretnie tego rodzaju przypadkami użycia. Zobacz djangorestframework-recursive .

Tom Christie
źródło
3
Ok, to działa dla głębokości = 1. A jeśli mam więcej poziomów w drzewie obiektów - kategoria ma podkategorię, która ma podkategorię? Chcę przedstawić całe drzewo o dowolnej głębokości za pomocą obiektów w wierszu. Korzystając z Twojego podejścia, nie mogę zdefiniować pola podkategorii w SubCategorySerializer.
Jacek Chmielewski
Edytowano z dodatkowymi informacjami na temat serializatorów odwołujących się do samych siebie.
Tom Christie,
Teraz mam KeyError at /api/category/ 'subcategories'. Przy okazji dzięki za superszybkie odpowiedzi :)
Jacek Chmielewski
4
Dla każdego, kto przeglądał to pytanie, zauważyłem, że dla każdego dodatkowego poziomu rekurencyjnego musiałem powtórzyć ostatnią linię w drugiej edycji. Dziwne obejście, ale wydaje się działać.
Jeremy Blalock
19
Chciałbym tylko zaznaczyć, że „base_fields” już nie działa. W DRF 3.1.0 „_declared_fields” to magia.
Travis Swientek
50

Rozwiązanie @ wjin działało świetnie, dopóki nie zaktualizowałem do Django REST framework 3.0.0, który przestaje być używany jako_native . Oto moje rozwiązanie DRF 3.0, które jest niewielką modyfikacją.

Załóżmy, że masz model z polem odwołującym się do siebie, na przykład komentarze z wątkami we właściwości o nazwie „odpowiedzi”. Masz drzewiastą reprezentację tego wątku komentarzy i chcesz serializować drzewo

Najpierw zdefiniuj klasę RecursiveField wielokrotnego użytku

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

Następnie dla swojego serializatora użyj RecursiveField, aby serializować wartość „odpowiedzi”

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

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

Proste, a do rozwiązania wielokrotnego użytku potrzebujesz tylko 4 wierszy kodu.

UWAGA: Jeśli struktura danych jest bardziej skomplikowana niż drzewo, na przykład skierowany wykres acykliczny (FANCY!), Możesz wypróbować pakiet @ wjin - zobacz jego rozwiązanie. Ale nie miałem żadnych problemów z tym rozwiązaniem dla drzew opartych na modelu MPTTM.

Mark Chackerian
źródło
1
Co robi linia serializer = self.parent.parent .__ class __ (value, context = self.context). Czy jest to metoda to_representation ()?
Mauricio
Ta linia jest najważniejszą częścią - umożliwia reprezentację pola w celu odniesienia się do właściwego serializatora. W tym przykładzie uważam, że byłby to CommentSerializer.
Mark Chackerian
1
Przepraszam. Nie mogłem zrozumieć, co robi ten kod. Uruchomiłem to i działa. Ale nie mam pojęcia, jak to właściwie działa.
Mauricio
Spróbuj print self.parent.parent.__class__print self.parent.parent
wstawić
Rozwiązanie działa, ale wynik zliczania mojego serializatora jest nieprawidłowy. Zlicza tylko węzły główne. Jakieś pomysły? Tak samo jest z djangorestframework-recursive.
Lucas Veiga
37

Inna opcja, która działa z 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
źródło
6
Dlaczego nie jest to akceptowana odpowiedź? Działa świetnie.
Karthik RP
5
Działa to bardzo prosto, miałem o wiele łatwiejszą pracę niż inne opublikowane rozwiązania.
Nick BL
To rozwiązanie nie wymaga dodatkowych zajęć i jest łatwiejsze do zrozumienia niż parent.parent.__class__rzeczy. Najbardziej mi się podoba.
SergiyKolesnikov
27

Spóźniłem się do gry tutaj, ale oto moje rozwiązanie. Powiedzmy, że serializuję Blah, z wieloma dziećmi również typu Blah.

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

Za pomocą tego pola mogę serializować rekurencyjnie zdefiniowane obiekty, które mają wiele obiektów podrzędnych

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

Napisałem rekursywne pole dla DRF3.0 i spakowałem je dla pip https://pypi.python.org/pypi/djangorestframework-recursive/

wjin
źródło
1
Działa z serializacją modelu MPTTM. Ładny!
Mark Chackerian
2
Nadal powtarzasz dziecko u źródła, chociaż? Jak mogę to zatrzymać?
Prometheus
Przepraszam @Sputnik Nie rozumiem, co masz na myśli. To, co tu podałem, działa w przypadku, gdy masz klasę Blahi ma ona pole o nazwie, child_blahsktóre składa się z listy Blahobiektów.
wjin
4
To działało świetnie, dopóki nie zaktualizowałem do DRF 3.0, więc opublikowałem wersję 3.0.
Mark Chackerian
1
@ Falcon1 Możesz filtrować queryset i przekazywać węzły główne tylko w widokach, takich jak queryset=Class.objects.filter(level=0). Zajmuje się resztą rzeczy samodzielnie.
chhantyal
13

Udało mi się osiągnąć ten wynik za pomocą pliku serializers.SerializerMethodField. Nie jestem pewien, czy to najlepszy sposób, ale zadziałało dla mnie:

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
źródło
1
U mnie sprowadzało się to do wyboru między tym rozwiązaniem a rozwiązaniem yprez . Są zarówno jaśniejsze, jak i prostsze niż wcześniej zamieszczone rozwiązania. Rozwiązanie tutaj wygrało, ponieważ stwierdziłem, że jest to najlepszy sposób na rozwiązanie problemu przedstawionego przez PO tutaj i jednoczesne wsparcie tego rozwiązania przy dynamicznym wybieraniu pól do serializacji . Rozwiązanie Ypreza powoduje nieskończoną rekursję lub wymaga dodatkowych komplikacji, aby uniknąć rekursji i odpowiednio wybrać pola.
Louis
9

Inną opcją byłoby powtórzenie w widoku, który serializuje model. Oto przykład:

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
źródło
To świetnie, miałem dowolnie głębokie drzewo, które musiałem serializować, a to działało jak urok!
Víðir Orri Reynisson
Dobra i bardzo przydatna odpowiedź. Podczas pobierania elementów podrzędnych na ModelSerializer nie można określić zestawu zapytań do pobierania elementów podrzędnych. W takim przypadku możesz to zrobić.
Efrin,
8

Niedawno miałem ten sam problem i wymyśliłem rozwiązanie, które wydaje się działać do tej pory, nawet dla dowolnej głębokości. Rozwiązaniem jest mała modyfikacja tego od Toma Christiego:

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

Nie jestem pewien, czy może niezawodnie działać w każdej sytuacji, chociaż ...

caipirginka
źródło
1
Od wersji 2.3.8 nie ma metody convert_object. Ale to samo można zrobić, zastępując metodę to_native.
abhaga
6

To jest adaptacja rozwiązania caipirginka, które działa na drf 3.0.5 i 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')

Zauważ, że CategorySerializer w 6. linii jest wywoływany z obiektem i atrybutem many = True.

Wicho Valdeavellano
źródło
Niesamowite, to zadziałało dla mnie. Jednak myślę, że if 'branches'należy zmienić naif 'subcategories'
vabada
5

Pomyślałem, że przyłączę się do zabawy!

Via wjin i Mark Chackerian stworzyłem bardziej ogólne rozwiązanie, które działa dla bezpośrednich modeli drzewiastych i struktur drzewiastych, które mają model przelotowy. Nie jestem pewien, czy to należy do jego własnej odpowiedzi, ale pomyślałem, że równie dobrze mogę to gdzieś umieścić. Dołączyłem opcję max_depth, która zapobiegnie nieskończonej rekurencji, na najgłębszym poziomie dzieci są reprezentowane jako adresy URL (jest to ostatnia klauzula else, jeśli wolisz, aby nie był to adres 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
źródło
To bardzo dokładne rozwiązanie, jednak warto zauważyć, że Twoja elseklauzula zawiera pewne założenia dotyczące widoku. Musiałem zamienić mój na, return value.pkwięc zwracał klucze podstawowe zamiast próbować odwrócić wygląd widoku.
Soviut
4

W Django REST framework 3.3.1 potrzebowałem następującego kodu, aby dodać podkategorie do kategorii:

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
źródło
1

To rozwiązanie jest prawie podobne do innych opublikowanych tutaj rozwiązań, ale ma niewielką różnicę pod względem problemu z powtarzaniem się dzieci na poziomie głównym (jeśli uważasz, że jest to problem). Dla przykładu

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

i jeśli masz taki pogląd

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

To da następujący wynik,

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

Tutaj parent categorymachild category a reprezentacja json jest dokładnie tym, co chcemy, aby była reprezentowana.

ale widać, że jest powtórzenie child categoryna poziomie głównym.

Ponieważ niektórzy ludzie pytają w sekcjach komentarzy powyżej opublikowanych odpowiedzi, że jak możemy zatrzymać to powtórzenie dziecka na poziomie głównym , po prostu przefiltruj swój zestaw zapytań za pomocą parent=None, jak poniżej

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

to rozwiąże problem.

UWAGA: Ta odpowiedź może nie być bezpośrednio związana z pytaniem, ale problem jest w jakiś sposób powiązany. Również takie podejście RecursiveSerializerjest kosztowne. Lepiej, jeśli używasz innych opcji, które są podatne na wydajność.

Md. Tanvir Raihan
źródło
Zestaw zapytań z filtrem spowodował dla mnie błąd. Ale to pomogło pozbyć się powtarzającego się pola. Zastąp metodę to_representation w klasie serializatora: stackoverflow.com/questions/37985581/…
Aaron