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-рекурсивний .


3
Добре, це працює для глибини = 1. Що робити, якщо в дереві об’єктів у мене більше рівнів - категорія має підкатегорію, яка має підкатегорію? Я хочу представити все дерево довільної глибини за допомогою вбудованих об'єктів. Використовуючи ваш підхід, я не можу визначити поле підкатегорії в SubCategorySerializer.
Яцек Хмелевський

Відредаговано з додатковою інформацією про самореференційні серіалізатори.
Том Крісті,

Тепер я зрозумів KeyError at /api/category/ 'subcategories'. До речі, дякую за ваші супершвидкі відповіді :)
Яцек Хмелевський

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 рядки коду для повторного використання рішення.

ПРИМІТКА: Якщо ваша структура даних складніша за дерево, наприклад, скажімо, спрямований ациклічний графік (FANCY!), Тоді ви можете спробувати пакет @ wjin - див. Його рішення. Але я не мав жодних проблем з цим рішенням для дерев на основі MPTTModel.


1
Що робить серіалізатор рядка = self.parent.parent .__ клас __ (значення, контекст = self.context). Це метод to_representation ()?
Маурісіо

Цей рядок є найважливішою частиною - він дозволяє в поданні поля посилатися на правильний серіалізатор. У цьому прикладі я вважаю, що це буде CommentSerializer.
Mark Chackerian

1
Вибачте. Я не міг зрозуміти, що робить цей код. Я запустив це, і воно працює. Але я не уявляю, як це насправді працює.
Маурісіо

Спробуйте ввести кілька заяв для друку, як print self.parent.parent.__class__іprint self.parent.parent
Марк Чакеріан

Рішення працює, але результат підрахунку мого серіалізатора помилковий. Він враховує лише кореневі вузли. Будь-які ідеї? Те саме і з djangorestframework-рекурсивно.
Лукас Вейга,

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

6
Чому це не прийнята відповідь? Працює ідеально.
Karthik RP

5
Це працює дуже просто, мені було набагато легше отримати цю роботу, ніж інші опубліковані рішення.
Nick BL

Це рішення не потребує додаткових занять і легше зрозуміти, ніж parent.parent.__class__матеріали. Мені це подобається найбільше.
Сергій Колесніков

27

Пізно до гри тут, але ось моє рішення. Скажімо, я серіалізую бла, з кількома дітьми також типу бла.

    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/


1
Працює з серіалізацією MPTTModel. Приємно!
Mark Chackerian

2
Ви все ще повторюєте дитину біля кореня? Як я можу це зупинити?
Прометей

Вибачте @Sputnik. Я не розумію, що ви маєте на увазі. Те, що я дав тут, працює для випадку, коли у вас є клас, Blahі в ньому є поле, child_blahsяке називається списком Blahоб’єктів.
wjin

4
Це працювало чудово, поки я не перейшов на DRF 3.0, тому я опублікував версію 3.0.
Mark Chackerian

1
@ Falcon1 Ви можете фільтрувати набір запитів і передавати лише кореневі вузли у таких поданнях, як queryset=Class.objects.filter(level=0). Він сам обробляє решту речей.
chhantyal

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, ви не можете вказати набір запитів для отримання дочірніх елементів. У цьому випадку ви можете це зробити.
Ефрін

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

Я не впевнений, що це може надійно працювати в будь-якій ситуації, хоча ...


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

6

Я думав, що приєднаюся до розваги!

Через 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'])

Це дуже ґрунтовне рішення, однак, варто зазначити, що ваш elseпункт робить певні припущення щодо подання. Мені довелося замінити мій return value.pkтаким чином, що він повернув первинні ключі, замість того, щоб намагатись переглядати подання назад.
Совіт

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

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 categoryhas child categoryі json - саме те, що ми хочемо, щоб воно було представлене.

але ви можете бачити, що є повторення child categoryна кореневому рівні.

Оскільки деякі люди запитують у розділах коментарів до опублікованих вище відповідей, що як ми можемо зупинити повторення цієї дочірньої групи на кореневому рівні , просто відфільтруйте набір запитів за допомогою parent=None, як показано нижче

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

це вирішить проблему.

ПРИМІТКА: Ця відповідь може не мати прямого відношення до запитання, але проблема якось пов’язана. Також такий підхід використання RecursiveSerializerє дорогим. Краще, якщо ви використовуєте інші варіанти, які схильні до ефективності.


Набір запитів із фільтром спричинив для мене помилку. Але це допомогло позбутися повторного поля. Перевизначити метод to_representation у класі серіалізатора: stackoverflow.com/questions/37985581/…
Аарон,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.