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.

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 .


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.


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

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/


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

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)

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ż ...


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.


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

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

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ść.


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.