Django rest framework вложенные самореферентные объекты

88

У меня есть модель, которая выглядит так:

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)

Мне удалось получить плоское json-представление всех категорий с помощью сериализатора:

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

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

Теперь я хочу, чтобы список подкатегорий имел встроенное json-представление подкатегорий вместо их идентификаторов. Как мне это сделать с помощью django-rest-framework? Я пытался найти его в документации, но он кажется неполным.

Яцек Хмелевски
источник

Ответы:

70

Вместо использования ManyRelatedField используйте в качестве поля вложенный сериализатор:

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

Если вы хотите иметь дело с произвольно вложенными полями, вам следует взглянуть на настройку полей по умолчанию в части документации. В настоящее время вы не можете напрямую объявить сериализатор как поле самого себя, но вы можете использовать эти методы, чтобы переопределить, какие поля используются по умолчанию.

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

На самом деле, как вы заметили, это не совсем так. Это своего рода уловка, но вы можете попробовать добавить поле после того, как сериализатор уже объявлен.

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

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

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

Необходимо добавить механизм объявления рекурсивных отношений.


Изменить : обратите внимание, что теперь доступен сторонний пакет, специально предназначенный для этого типа использования. См. Djangorestframework-recursive .

Том Кристи
источник
3
Хорошо, это работает для глубины = 1. Что делать, если в дереве объектов больше уровней - у категории есть подкатегория, у которой есть подкатегория? Я хочу представить все дерево произвольной глубины с помощью встроенных объектов. Используя ваш подход, я не могу определить поле подкатегории в SubCategorySerializer.
Jacek Chmielewski
Отредактировано с дополнительной информацией о сериализаторах с ссылками на самих себя.
Tom Christie
Теперь я получил KeyError at /api/category/ 'subcategories'. Кстати, спасибо за сверхбыстрые ответы :)
Jacek Chmielewski
4
Для всех, кто впервые просматривает этот вопрос, я обнаружил, что для каждого дополнительного рекурсивного уровня мне приходилось повторять последнюю строку во втором редактировании. Странное решение, но вроде работает.
Джереми Блэлок
19
Я просто хочу отметить, что base_fields больше не работает. В DRF 3.1.0 «_declared_fields» - вот где волшебство.
Трэвис Свентек,
50

Решение @wjin отлично работало для меня, пока я не обновился до Django REST framework 3.0.0, который не поддерживает to_native . Вот мое решение DRF 3.0, которое представляет собой небольшую модификацию.

Допустим, у вас есть модель с самореференциальным полем, например, связанные комментарии в свойстве, называемом «ответы». У вас есть древовидное представление этой цепочки комментариев, и вы хотите сериализовать дерево

Сначала определите свой многоразовый класс RecursiveField

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

Затем для сериализатора используйте RecursiveField, чтобы сериализовать значение «ответов».

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

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

Легко и просто, и вам нужно всего 4 строки кода для решения, которое можно использовать повторно.

ПРИМЕЧАНИЕ. Если ваша структура данных более сложна, чем дерево, например, ориентированный ациклический граф (ФАНТАЗИЯ!), Вы можете попробовать пакет @wjin - см. Его решение. Но у меня не было никаких проблем с этим решением для деревьев на основе MPTTModel.

Марк Чакериан
источник
1
Что делает строка serializer = self.parent.parent .__ class __ (value, context = self.context). Это метод to_presentation ()?
Маурисио
Эта строка является наиболее важной частью - она ​​позволяет представлению поля ссылаться на правильный сериализатор. В этом примере я полагаю, что это будет CommentSerializer.
Марк Чакериан
1
Мне жаль. Я не мог понять, что делает этот код. Я запустил его, и он работает. Но я понятия не имею, как это на самом деле работает.
Маурисио
Попробуйте print self.parent.parent.__class__print self.parent.parent
вставить
Решение работает, но вывод счетчика моего сериализатора неверен. Он учитывает только корневые узлы. Любые идеи? То же самое и с djangorestframework-recursive.
Лукас Вейга
37

Другой вариант, который работает с 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
источник
6
Почему это не принятый ответ? Прекрасно работает.
Karthik RP
5
Это работает очень просто, мне было намного проще заставить это работать, чем другие опубликованные решения.
Nick BL
Это решение не требует дополнительных классов и его легче понять, чем parent.parent.__class__материал. Мне это нравится больше всего.
Сергей Колесников
27

Поздно к игре, но вот мое решение. Скажем, я сериализирую Blah с несколькими дочерними элементами также типа Blah.

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

Используя это поле, я могу сериализовать свои рекурсивно определенные объекты, у которых есть много дочерних объектов.

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

Я написал рекурсивное поле для DRF3.0 и упаковал его для pip https://pypi.python.org/pypi/djangorestframework-recursive/

Wjin
источник
1
Работает с сериализацией модели MPTTM. Ницца!
Марк Чакериан
2
У вас все еще повторяется ребенок в корне, хотя? Как я могу это остановить?
Прометей
Извините, @Sputnik, я не понимаю, о чем вы. То, что я здесь Blahпривел, работает для случая, когда у вас есть класс и у него есть поле с именем, child_blahsкоторое состоит из списка Blahобъектов.
wjin 08
4
Это отлично работало, пока я не обновился до DRF 3.0, поэтому я опубликовал вариант 3.0.
Марк Чакериан 01
1
@ Falcon1 Вы можете фильтровать набор запросов и передавать только корневые узлы в представлениях, например queryset=Class.objects.filter(level=0). Все остальное он берет на себя.
чхантял
13

Мне удалось добиться этого результата с помощью файла serializers.SerializerMethodField. Я не уверен, что это лучший способ, но сработал для меня:

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
Для меня это сводилось к выбору между этим решением и решением yprez . Они и понятнее, и проще, чем решения, опубликованные ранее. Решение здесь победило, потому что я обнаружил, что это лучший способ решить проблему, представленную здесь OP, и в то же время поддержать это решение для динамического выбора полей для сериализации . Решение Yprez вызывает бесконечную рекурсию или требует дополнительных усложнений, чтобы избежать рекурсии и правильно выбрать поля.
Луи
9

Другой вариант - выполнить рекурсию в представлении, которое сериализует вашу модель. Вот пример:

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)
Стефан Рейнхард
источник
Это здорово, у меня было произвольно глубокое дерево, которое мне нужно было сериализовать, и это работало как шарм!
Víðir Orri Reynisson
Хороший и очень полезный ответ. При получении дочерних элементов в ModelSerializer вы не можете указать набор запросов для получения дочерних элементов. В этом случае вы можете это сделать.
Efrin
8

Недавно у меня была такая же проблема, и я пришел к решению, которое, кажется, работает до сих пор даже для произвольной глубины. Решение представляет собой небольшую модификацию решения Тома Кристи:

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

Я не уверен, что он может надежно работать в любой ситуации ...

Caipirginka
источник
1
Начиная с 2.3.8, нет метода convert_object. Но то же самое можно сделать, переопределив метод to_native.
abhaga
6

Это адаптация решения caipirginka, которое работает на drf 3.0.5 и 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')

Обратите внимание, что CategorySerializer в 6-й строке вызывается с объектом и атрибутом many = True.

Вичо Вальдеавельяно
источник
Удивительно, у меня это сработало. Тем не менее, я думаю, что if 'branches'следует изменить наif 'subcategories'
vabada
5

Я думал, что присоединюсь к веселью!

Через wjin и Mark Chackerian я создал более общее решение, которое работает для прямых древовидных моделей и древовидных структур, имеющих сквозную модель. Я не уверен, относится ли это к его собственному ответу, но я подумал, что мог бы его где-нибудь положить. Я включил параметр max_depth, который предотвратит бесконечную рекурсию, на самом глубоком уровне дочерние элементы представлены как URL-адреса (это последнее предложение else, если вы предпочитаете, чтобы это не был 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'])
Будет S
источник
Это очень тщательное решение, однако стоит отметить, что ваше elseпредложение делает определенные предположения относительно представления. Мне пришлось заменить мой на, return value.pkчтобы он возвращал первичные ключи вместо попытки обратного просмотра представления.
Soviut
4

В Django REST framework 3.3.1 мне понадобился следующий код для добавления подкатегорий в категории:

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
источник
1

Это решение почти аналогично другим решениям, размещенным здесь, но имеет небольшую разницу с точки зрения проблемы повторения дочерних элементов на корневом уровне (если вы думаете, что это проблема). Для примера

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

и если у вас есть это мнение

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

Это даст следующий результат:

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

Здесь parent categoryесть a, child categoryа представление json - это именно то, что мы хотим представить.

но вы можете видеть, что child categoryна корневом уровне есть повторение .

Поскольку некоторые люди спрашивают в разделах комментариев к опубликованным выше ответам, как мы можем остановить это дочернее повторение на корневом уровне , просто отфильтруйте свой набор запросов parent=None, как показано ниже

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

это решит проблему.

ПРИМЕЧАНИЕ. Этот ответ может не иметь прямого отношения к вопросу, но проблема каким-то образом связана. К тому же такой подход к использованию RecursiveSerializerстоит дорого. Лучше, если вы будете использовать другие варианты, которые зависят от производительности.

Md. Tanvir Raihan
источник
Набор запросов с фильтром вызвал у меня ошибку. Но это помогло избавиться от повторяющегося поля. Переопределить метод to_presentation в классе сериализатора: stackoverflow.com/questions/37985581/…
Аарон