Как в Django REST Framework сериализовать связанные модели или добавить вычисляемые поля

Ответ

Для работы со сложными структурами данных в Django REST Framework (DRF) используются несколько подходов:

  1. Вложенные сериализаторы (Nested Serializers): Позволяют включать представления связанных моделей (OneToOne, ForeignKey, ManyToMany) в родительский объект.

  2. SerializerMethodField: Используется для добавления в сериализатор вычисляемых полей, значение которых определяется методом самого сериализатора. Это идеально подходит для агрегаций, кастомной логики или данных, не хранящихся в модели напрямую.

  3. Переопределение методов .create() и .update(): Необходимо для реализации записи во вложенные сериализаторы, так как DRF по умолчанию не умеет обрабатывать создание/обновление связанных объектов.

Пример: Сериализатор профиля пользователя, который включает данные самого пользователя и вычисляет количество его постов.

from rest_framework import serializers
from django.contrib.auth.models import User

# Сериализатор для вложенной модели User
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']

class ProfileSerializer(serializers.ModelSerializer):
    # 1. Вложенный сериализатор (только для чтения по умолчанию)
    user = UserSerializer()

    # 2. Вычисляемое поле
    posts_count = serializers.SerializerMethodField()

    class Meta:
        model = Profile
        fields = ['id', 'user', 'bio', 'posts_count']

    def get_posts_count(self, obj):
        # obj - это экземпляр модели Profile
        return obj.posts.count()

    # 3. Переопределение create для поддержки записи вложенных данных
    def create(self, validated_data):
        user_data = validated_data.pop('user')
        user = User.objects.create_user(**user_data)
        profile = Profile.objects.create(user=user, **validated_data)
        return profile

Оптимизация: Для предотвращения проблемы N+1 при работе с такими сериализаторами в ViewSet следует использовать select_related (для ForeignKey) и prefetch_related (для ManyToMany) в queryset.

Ответ 18+ 🔞

А, ну это же про эти ваши вложенные штуки в DRF, да? Ну, слушай, тут всё просто, как три копейки, если не усложнять.

Вот представь: у тебя есть модель Profile, которая привязана к стандартной User через OneToOneField. И тебе надо отдавать в одном JSON и данные профиля, и данные юзера, и ещё какую-нибудь хуйню посчитать, типа количества его постов. DRF из коробки так не умеет, ему надо помочь, но не сильно.

Подход первый — вложенный сериализатор.
Берёшь сериализатор для User, объявляешь его как поле в сериализаторе Profile. Всё, теперь в ответе будет объект user со всеми его полями. Красота! Но есть нюанс, ёпта: по умолчанию это поле только для чтения. Если попробуешь через него создать профиль с юзером — получишь пиздюлину в виде ошибки. DRF тебе вежливо скажет: «Сам разбирайся, я не умею».

Подход второй — SerializerMethodField.
Это твой волшебный пендель, когда надо добавить в вывод что-то, чего в модели нет. Хочешь посчитать посты? Пожалуйста! Объявляешь поле posts_count как SerializerMethodField и пишешь метод get_posts_count, который тупо возвращает obj.posts.count(). Всё, поле появилось в JSON. Главное — помни, что это поле только для чтения, его нельзя использовать для записи. Не пытайся, а то опять будет пиздец.

Подход третий — переопределение .create() и .update().
Вот тут начинается магия, а может, и пиздец, смотря как руки из жопы растут. Если тебе нужно создать профиль и юзера за один запрос, то в методе create() сериализатора ты должен сам распаковать данные для юзера, создать его, а потом уже создать профиль. Пример в коде ниже — смотри, как я это сделал. С update() история аналогичная, только ещё веселее, потому что надо и обновлять, и проверять, что там уже есть.

А теперь про оптимизацию, а то без неё твой сервер накроется медным тазом от N+1 запросов.
Ты в ViewSet в queryset обязательно добавляешь select_related для связей ForeignKey и OneToOne (чтобы джойнить таблицы) и prefetch_related для ManyToMany (чтобы не делать тысячу запросов за связанными объектами). Иначе на каждую запись профиля DRF полезет в базу за юзером отдельным запросом — это пиздец, а не производительность.

from rest_framework import serializers
from django.contrib.auth.models import User

# Сериализатор для вложенной модели User
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']

class ProfileSerializer(serializers.ModelSerializer):
    # 1. Вложенный сериализатор (только для чтения по умолчанию)
    user = UserSerializer()

    # 2. Вычисляемое поле
    posts_count = serializers.SerializerMethodField()

    class Meta:
        model = Profile
        fields = ['id', 'user', 'bio', 'posts_count']

    def get_posts_count(self, obj):
        # obj - это экземпляр модели Profile
        return obj.posts.count()

    # 3. Переопределение create для поддержки записи вложенных данных
    def create(self, validated_data):
        user_data = validated_data.pop('user')
        user = User.objects.create_user(**user_data)
        profile = Profile.objects.create(user=user, **validated_data)
        return profile

Вот и вся философия. Сначала кажется, что овердохуища сложного, а на деле — пара методов переписать и не забыть про select_related. Главное — не выёбывайся со сложными структурами без необходимости, а то сам потом в этом коде будешь разбираться, как слепой крот в лабиринте.