Эффективное использование DRF-сериализаторов в Django

Для чтения данной статьи потребуются базовые знания по Django REST фреймворку.

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

Мы обсудим следующие вопросы:

  • несколько способов использовать аргумент source сериализатора;
  • как и когда использовать метод SerializerMethodField;
  • как и когда использовать метод to_representation;

Как использовать ключевой аргумент source

DRF-сериализатор имеет ключевой аргумент под названием source, который отличается гибкостью и может помочь избежать ряда типичных ошибок.

Давайте напишем сериализатор, способный создавать серийные представления класса User:

from rest_framework import serializers

from django.contrib.auth.models import User


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'first_name', 'last_name') 

Теперь давайте произведем эту сериализацию:

In [1]: from accounts.serializers import UserSerializer
In [2]: from django.contrib.auth.models import User
In [3]: user = User.objects.latest('pk')
In [4]: serializer = UserSerializer(user)In [5]: serializer.data
Out[5]: {'username': 'akshar', 'email': 'akshar@agiliq.com', 'first_name': '', 'last_name': ''}

Представим, что во фронтенде или в мобильном приложении, использующем такой API, требуется, чтобы ключ в серийном представлении был user_name вместо username.

Чтобы добиться этого, мы можем добавить в код метод CharField с ключевым аргументом source:

class UserSerializer(serializers.ModelSerializer):

    user_name = serializers.CharField(source='username')

    class Meta:
        model = User
        fields = ('user_name', 'email', 'first_name', 'last_name')

Давайте убедимся, что поле username заменилось на user_name в Meta.fields. Перезапустим оболочку и снова создадим экземпляр сериализатора:

In [6]: serializer = UserSerializer(user)
In [7]: serializer.data
Out[7]: {'user_name': 'akshar', 'email': 'akshar@agiliq.com', 'first_name': '', 'last_name': ''}

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

В классе User есть метод под названием get_full_name. Давайте установим в нем поля first_name и last_name.

In [8]: user.first_name = 'akshar'
In [9]: user.last_name = 'raaj'
In [10]: user.save()
In [11]: user.get_full_name()
Out[11]: 'akshar raaj'

Давайте добавим в сериализатор поле под названием full_name . Свяжем аргумент source c User.get_full_name.

class UserSerializer(serializers.ModelSerializer):

    user_name = serializers.CharField(source='username')
    full_name = serializers.CharField(source='get_full_name')

    class Meta:
        model = User
        fields = ('user_name', 'email', 'first_name', 'last_name', 'full_name') 

Теперь перезапустим оболочку и получим сериализованное представление класса User:

In [3]: user = User.objects.latest('pk')
In [4]: serializer = UserSerializer(user)
In [5]: serializer.data
Out[5]: {'user_name': 'akshar', 'email': 'akshar@agiliq.com', 'first_name': 'akshar', 'last_name': 'raaj', 'full_name': 'akshar raaj'}

Обратите внимание, каким образом в поле full_name появляется желаемый результат. Под капотом DRF использует метод get_full_name для заполнения поля full_name.

Аргумент source также может без проблем взаимодействовать с отношениями, то есть с ForeignKey, OneToOneField и ManyToMany.

Представим, у нас есть модель Profile , которая имеет отношения один-к-одному (OneToOneField) с классом User.

Эта модель имеет следующий вид:

class Profile(models.Model):

    user = models.OneToOneField(User, on_delete=models.CASCADE)
    street = models.CharField(max_length=100)
    city = models.CharField(max_length=100)

    def get_full_address(self):
        return "%s, %s" % (self.street, self.city) 

Допустим, мы хотим, чтобы названия улицы и города отправлялись в сериализованном представлении. Тогда мы могли бы добавить поле улицы и поле города в сериализатор и передать их в аргумент source.

class UserSerializer(serializers.ModelSerializer):

    street = serializers.CharField(source='profile.street')
    city = serializers.CharField(source='profile.city')

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name', 'street', 'city') 

Давайте перезапустим оболочку и посмотрим на результат:

In [4]: user = User.objects.latest('pk')
In [5]: serializer = UserSerializer(user)
In [6]: serializer.data
Out[6]: {'email': 'akshar@agiliq.com', 'first_name': 'akshar', 'last_name': 'raaj', 'street': 'Pennsylvania Avenue', 'city': 'Washington'}

Подобно тому как аргумент source работает с методами самого объекта, он также легко взаимодействует и с методами связанных объектов.

Мы хотим получить полный адрес пользователей, доступный с помощью метода user.profile.get_full_address ().

В данном случае мы можем просто передать в аргумент source метод profile.get_full_address.

class UserSerializer(serializers.ModelSerializer):

    full_address = serializers.CharField(source='profile.get_full_address')

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name', 'full_address') 

Опять сериализуем класс User и получим:

In [3]: user = User.objects.latest('pk')
In [4]: serializer = UserSerializer(user)
In [5]: serializer.data
Out[5]: {'email': 'akshar@agiliq.com', 'first_name': 'akshar', 'last_name': 'raaj', 'full_address': 'Pennsylvania Avenue, Washington'}

Теперь давайте посмотрим, как легко работает аргумент source с отношением многие-ко-многим (ManyToManyField).

Мы также хотим получить связанные группы пользователей в сериализованном представлении. Давайте для начала добавим в класс User несколько групп.

In [12]: g1 = Group.objects.create(name='BBC')
In [13]: g2 = Group.objects.create(name='Sony')
In [15]: user.groups.add(*[g1, g2])

Для каждой группы понадобятся поля id и name.

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

Выглядеть он будет следующим образом:

from django.contrib.auth.models import Group

class GroupSerializer(serializers.ModelSerializer):

class Meta:
    model = Group
    fields = ('id', 'name') 

Наивный способ добавить группы к сериализованным данным — это определить класс SerializerMethodField и добавить в него метод user.groups.all().

А способ, отвечающий духу DRF, состоит в том, чтобы добавить поле all_groups в сериализатор и установить его значение при помощи класса GroupSerializer.

class GroupSerializer(serializers.ModelSerializer):

    class Meta:
        model = Group
        fields = ('id', 'name')


class UserSerializer(serializers.ModelSerializer):

    all_groups = GroupSerializer(source='groups', many=True)

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name', 'all_groups') 

Давайте произведем сериализацию и убедимся в том, что информация из связанной группы присутствует в сериализованных данных.

In [1]: from accounts.serializers import UserSerializer
In [2]: from django.contrib.auth.models import User
In [3]: user = User.objects.latest('pk')
In [5]: serializer = UserSerializer(user)
In [6]: serializer.data
Out[6]: {'email': 'akshar@agiliq.com', 'first_name': 'akshar', 'last_name': 'raaj', 'groups': [OrderedDict([('id', 2), ('name', 'BBC')]), OrderedDict([('id', 3), ('name', 'Sony')])]}

DRF достаточно умен, чтобы вызвать user.groups.all(), хотя мы просто установили аргумент source = groups. DRF заключает, что раз groups имеет тип ManyRelatedManager, то для получения всех связанных групп надо использовать .all().

Если мы не хотим предоставлять групповую информацию по POST-запросу, мы можем добавить ключевой аргумент read_only=True в GroupSerializer.

Допустим, у нас есть модель под названием Article и она имеет отношение один-ко-многим (ForeignKey) к классу User. Тогда мы можем добавлять статьи (articles) пользователя (User) в сериализованное представление следующим образом:

articles = ArticleSerializer(source='article_set', many=True)

Как и когда использовать метод SerializerMethodField

Бывают случаи, когда вам нужно запустить какой-то собственный код во время сериализации определенного поля.

Вот несколько примеров:

  • преобразование поля first_name в регистр заголовка во время сериализации;
  • преобразование поля full_name в верхний регистр;
  • установка значения groups в None вместо пустого списка — на тот случай, если с конкретным пользователем не связаны никакие группы;

Рассмотрим первый сценарий. Мы хотим изменить значение поля first_name в регистр заголовка (написание с заглавной буквы) в процессе сериализации.

До сих пор мы не хотели запускать никакой собственный код для поля first_name, поэтому наличия first_name в полях Meta.fields было достаточно. Сейчас мы хотим запустить некоторый собственный код, поэтому нам нужно будет явно определить поле first_name как экземпляр класса SerializerMethodField.

class UserSerializer(serializers.ModelSerializer):

    first_name = serializers.SerializerMethodField()

    def get_first_name(self, obj):
        return obj.first_name.title()

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name') 

Когда поле имеет тип SerializerMethodField, DRF вызывает метод get_ при вычислении значения для этого поля. Аргумент obj здесь является экземпляром класса user.

Перезапустим оболочку и произведем сериализацию:

In [4]: user = User.objects.latest('pk')
In [5]: serializer = UserSerializer(user)
In [6]: serializer.data
Out[6]: {'email': 'akshar@agiliq.com', 'first_name': 'Akshar', 'last_name': 'raaj'}

Обратите внимание, что имя (значение поля first_name) теперь пишется с заглавной буквы.

Если мы хотим преобразовать значение поля full_name в верхний регистр, нам нужно будет изменить сериализатор следующим образом:

class UserSerializer(serializers.ModelSerializer):

    full_name = serializers.SerializerMethodField()

    def get_full_name(self, obj):
        return obj.get_full_name().upper()

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name', 'full_name') 

Опять произведем сериализацию и увидим:

In [3]: user = User.objects.latest('pk')
In [4]: serializer = UserSerializer(user)
In [5]: serializer.data
Out[5]: {'email': 'akshar@agiliq.com', 'first_name': 'akshar', 'last_name': 'raaj', 'full_name': 'AKSHAR RAAJ'}

Если же мы захотим установить значение groups в None вместо пустого списка, наш сериализатор примет следующий вид:

class UserSerializer(serializers.ModelSerializer):

    groups = serializers.SerializerMethodField()

    def get_groups(self, obj):
        groups = obj.groups.all()
        if not groups:
            return None
        return GroupSerializer(groups, many=True).data

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name', 'groups')

Как и когда использовать метод to_representation

Сериализаторы в DRF имеют полезный метод под названием to_representation.

Предположим, мы хотим добавлять ключ под названием admin к сериализованным данным, только когда пользователь является суперпользователем. А для остальных пользователей этот ключ в сериализованных данных присутствовать не должен.

Тогда наш сериализатор будет иметь следующий вид:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('email', 'first_name', 'last_name')

    def to_representation(self, instance):
        representation = super().to_representation(instance)
        if instance.is_superuser:
            representation['admin'] = True
        return representation 

Аргумент instance является экземпляром класса User.

Давайте проведем сериализацию для суперюзера.

In [2]: user = User.objects.latest('pk')
In [5]: serializer = UserSerializer(user)
In [6]: serializer.data
Out[6]: {'email': 'akshar@agiliq.com', 'first_name': 'akshar', 'last_name': 'raaj', 'admin': True}

А теперь отметим пользователя как не-суперюзера и сделаем тоже самое:

In [7]: user.is_superuser = False
In [8]: user.save()
In [9]: serializer = UserSerializer(user)
In [10]: serializer.data
Out[10]: {'email': 'akshar@agiliq.com', 'first_name': 'akshar', 'last_name': 'raaj'}

Обратите внимание, что ключ admin отсутствует в сериализованных данных для не-суперюзера.

Выводы

Мы рассмотрели поведение сериализатора Django при чтении. Если вы хотите разобраться, как эффективно использовать сериализаторы при операция записи, ждите продолжение.