Django Rest Framework для начинающих: как работает ModelSerializer

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В предыдущих статьях мы подробно разобрали работу сериалайзера на основе классов BaseSerializer и Serializer, и теперь мы можем перейти к классу-наследнику ModelSerializer.


image


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


  • автоматическое создание полей сериалайзера на основе данных о корреспондирующих полях модели;
  • автоматическое включение в поля сериалайзера тех же валидаторов, которые есть в полях модели, а также при определённых условиях метавалидаторов;
  • заранее определённые методы create и update.

Общие принципы работы модельного сериалайзера как на чтение, так и на запись идентичны тому, как работает базовый класс Serializer.


ModelSerializer: необходимый минимум


Для определения модельного сериалайзера нужен внутренний класс Meta со следующими атрибутами:


  • model — это джанго-модель, которую будет обслуживать сериалайзер. Модель не должна быть абстрактной.
  • Один из следующих атрибутов:
    • fields — поля джанго-модели, для которых будут созданы корреспондирующие поля сериалайзера;
    • exclude — поля джанго-модели, для которых не нужно создавать поля сериалайзера, но они будут созданы для всех остальных.

Автоматически создаваемые и декларируемые поля


Автоматически создаваемые поля. Поля сериалайзера, имена которых указаны во внутреннем классе Meta явно через fields или неявно через exclude, DRF создаст самостоятельно, сопоставив с одноимёнными полями модели. Если одноимённого поля модели не найдётся, будет выброшено исключение через метод build_unknown_field.


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


class TownSerializer(serializers.ModelSerializer):
   name = serializers.CharField(allow_blank=True)

   class Meta:
      model = Town
      fields = ['id', 'writers', 'name']

Поле name — декларируемое, класс поля и его атрибуты мы задали самостоятельно. Поля id и writers — автоматически создаваемые.


Обратите внимание, что name также включено в список fields. Дело в том, что, если присутствует атрибут fields, в нём должны быть перечислены все поля сериалайзера, в том числе и декларируемые.


Декларируемые поля могут понадобится, когда нам не подходит класс поля, который модельный сериалайзер подберёт автоматически. А ещё через декларируемые поля зачастую используют вспомогательные классы ReadOnlyField, HiddenField, SerializerMethodField.


Если вас устраивает класс поля сериалайзера, но нужна тонкая настройка его параметров, включая создание нескольких полей сериалайзера для одного поля модели, скорее всего, будет достаточно воспользоваться атрибутом extra_kwargs внутреннего класса Meta.


Что означает fields = '__all__'


Если в качестве значения fields выступает строка __all__, то в сериалайзере будут созданы поля для обслуживания всех полей модели, кроме тех, за работу с которыми будут отвечать декларируемые поля сериалайзера. Использование __all__ отменяет необходимость прописывать в fields декларируемые поля.


Как правильно использовать fields и exclude


На основе исходного кода DRF можно сформулировать несколько правил.


  • fields и exclude нельзя использовать вместе.
  • exclude должен быть либо списком, либо кортежем из названий полей модели, даже если поле одно.
  • fields может быть задан в виде списка либо кортежа (даже если поле одно) или строки __all__.

Примеры модельного сериалайзера для джанго-модели Writer:


class Writer(models.Model):
    firstname = models.CharField(max_length=100...)
    lastname = models.CharField(max_length=100...)
    patronymic = models.CharField(max_length=100...)
    birth_place = models.ForeignKey(to=Town...)
    birth_date = models.DateField(...)

Сериалайзер, который будет обслуживать все поля модели:


class WriterModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = Writer
        fields = '__all__'

Сериалайзер, который будет обслуживать только поля firstname и lastname:


class WriterModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = Writer
        fields = ['firstname', 'lastname']

Сериалайзер, который будет обслуживать все поля, кроме firstname и lastname:


class WriterModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = Writer
        exclude = ['firstname', 'lastname']

Как DRF создаёт поля для модельного сериалайзера


Сбор информации о модели и распределение полей по группам. Сначала DRF собирает детальную информацию о полях модели. Для этого он задействует метод get_field_info из rest_framework.utils.model_meta. Результат — именованный кортеж FieldResult, в котором есть следующие элементы:


  • relations — словарь, объединяющий поля отношений модели ("forward_relations"), а также объекты класса RelatedManager ("reverse_relations");
  • fields_and_pk — словарь с остальными полями модели.

Для подбора корреспондирующих полей DRF использует три метода:


  • build_relational_field — для полей из relations;
  • build_property_field — для методов и property-атрибутов модели;
  • build_standard_field — для полей из fields_and_pk.

Рассмотрим на примерах:


class Town(models.Model):
    name = models.CharField(max_length=100, unique=True)

class Writer(models.Model):
    firstname = models.CharField(max_length=100...)
    lastname = models.CharField(max_length=100...)
    patronymic = models.CharField(max_length=100...)
    birth_place = models.ForeignKey(to=Town, to_field='name', related_name='writers'...)
    birth_date = models.DateField(...)

    def get_full_name(self):
        return f'{self.firstname} {self.patronymic} {self.lastname}'

Из модели Town DRF возьмёт:


  • поле name и поместит его в fields_and_pk для дальнейшей обработки методом build_standard_field;
  • менеджер writers, который связывает модель Town с моделью Writer. writers — это значение атрибута related_name поля birth_place в связанной модели. Менеджер будет помещён в relations и в дальнейшем обработан методом build_relational_field или методом build_nested_field.

Из модели Writer DRF возьмёт:


  • поля firstname, lastname, patronymic, birth_date и поместит их в fields_and_pk для дальнейшей обработки методом build_standard_field;
  • поле birth_place, которое поместит в relations для дальнейшей обработки методом build_relational_field или методом build_nested_field;
  • метод get_full_name, который переадресует методу build_property_field после проверки hasattr(model_class, field_name).

Подбор классов полей сериалайзера для стандартных полей модели. Стандартные поля модели — это поля, которые не относятся к полям отношений. В классе ModelSerializer есть атрибут, по которому DRF решает, какой класс поля сериалайзера подобрать для конкретного поля модели. Это атрибут serializer_fields_mapping. При необходимости его можно дополнить или переопределить.


По дефолту классы полей модели и сериалайзера сопоставляются так:


Класс поля сериалайзера Класс поля модели
1 BooleanField BooleanField, NullBooleanField
2 CharField CharField, TextField
3 DateField DateField
4 DateTimeField DateTimeField
5 DecimalField DecimalField
6 DurationField DurationField
7 EmailField EmailField
8 FileField FileField
9 FilePathField FilePathField
10 FloatField FloatField
11 ImageField ImageField
12 IPAddressField GenericIPAddressField
13 IntegerField AutoField, BigIntegerField, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, SmallIntegerField
14 SlugField SlugField
15 TimeField TimeField
16 URLField URLField
17 UUIDField UUIDField

Вне serializer_fields_mapping описана логика сопоставления полей JSONField, а также специфичных полей для PostgreSQL.


Подбор классов для полей отношений. Для ForeignKey-полей модели в модельном сериалайзере могут создаваться поля одного из двух классов:


  • SlugRelatedField, если в поле модели задан атрибут to_field;
  • PrimaryKeyRelatedField во всех иных случаях.

Для полей ManyToMany и обратных связей (объектов RelatedManager) создаётся поле класса PrimaryKeyRelatedField.


Важно: названия автоматически создаваемых полей сериалайзера для обратных связей нужно явно указывать в атрибуте fields класса Meta. Строка __all__ не включает в себя названия объектов RelatedManager-модели.


class Town(models.Model):
    name = models.CharField(max_length=100, unique=True)

class Writer(models.Model):
    # другие поля опускаем для краткости
    birth_place = models.ForeignKey(to=Town, to_field='name', related_name='writers',...)

class TownSerializer(serializers.ModelSerializer):
   class Meta:
      model = Town
      fields = '__all__' # в сериалайзере будет создано два поля: `id` и `name`
#ИЛИ  fields = ['id', 'name', 'writers'] — будет дополнительно создано поле `writers` 
# для доступа к записям в связанной модели

class WriterSerializer(serialziers.ModelSerializer):
   class Meta:
      model = Writer
      fields = '__all__' # в сериалайзере будут все поля модели, включая `birth_place`,
      # т.к. `forward_relations` включаются в `__all__`. 
      # для поля `birth_place` будет подобран класс `SlugRelatedField`, поскольку
      # в корреспондирующем поле модели установлен атрибут `to_field`.

Работа с методами модели и проперти. В этом случае ничего особенного не происходит: DRF создаёт ReadOnlyField, которое участвует только при работе сериалайзера на чтение. Без какой-либо дополнительной валидации оно возвращает значение из метода или проперти модели.


Работа с вложенными объектами и атрибут depth. Для примера возьмем сериалайзер из предыдущего раздела и используем его для чтения первой записи из модели:


class TownSerializer(serializers.ModelSerializer):
   class Meta:
      model = Town
      fields = ['id', 'name', 'writers']

s = TownSerializer(instance=Town.objects.first())
print(s.data)
-------------
{'id': 1, 'name': 'Вологда', 'writers': [6, 7]}

По ключу writers мы получили список из айдишников писателей, которые родились в Вологде.
Если мы хотим раскрыть информацию о вложенных объектах, поможет атрибут depth класса Meta. Устанавливаем ему значение 1, и тогда сериалайзер раскрывает содержимое объектов из списка writers.


class TownSerializer(serializers.ModelSerializer):
   class Meta:
      model = Town
      depth = 1
      fields = ['id', 'name', 'writers']

s = TownSerializer(instance=Town.objects.first())
print(s.data)
-------------
{
   'id': 1,
   'name': 'Вологда',
   'writers': [
                  {'id': 6, 'firstname': 'Варлам', 'lastname': 'Шаламов', ...},
                  {'id': 7, 'firstname': 'Константин', 'lastname': 'Батюшков', ...}
              ]
}

Если установлен атрибут depth=1, включается метод build_nested_field. Тогда поле сериалайзера, которое отвечает за поле отношения модели или объект RelatedManager, становится объектом класса NestedSerializer. Его код очень прост:


class NestedSerializer(ModelSerializer):
   class Meta:
       model = relation_info.related_model
       depth = nested_depth - 1
       fields = '__all__'

Фактически объект из связанной модели обрабатывает сериалайзер внутри сериалайзера и выдаёт словарь со всеми полями этого объекта — fields = '__all__'.
Вполне возможно, что все поля связанных объектов не нужны. Выход — явно указать, какой сериалайзер должен обслуживать прямые или обратные связи модели.


extra_kwargs: тонкая настройка автоматически создаваемых полей


Атрибут extra_kwargs определяют во внутреннем классе Meta. Это словарь, ключами которого выступают поля из fields или любого поля модели из тех, которые не перечислены в exclude. Значением для каждого ключа служит словарь с атрибутами, которыми нужно дополнить то или иное поле сериалайзера.


Допустим, мы хотим, чтобы сериалайзер для модели Town работал так:


  • на чтение возвращал названия городов из столбца name не в виде {'name': 'название_города'}, а в виде {'town': 'название_города'};
  • на запись получал данные для столбца name в виде {'name': 'название_города'}.

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


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


class Town(models.Model):
    name = models.CharField(max_length=100, unique=True)

class TownModelSerializer(serializers.ModelSerializer):

    class Meta:
        model = Town
        fields = ['town', 'name']
        extra_kwargs = {
            'town': {'source': 'name', 'read_only': True},
            'name': {'write_only': True}
        }

TownModelSerializer(instance=Town.objects.first()).data вернёт {'town': 'Вологда'}.


TownModelSerializer(data={'name': 'Анапа'}) после валидации вернёт в validated_data словарь {'name': 'Анапа'}.


Итак, какую донастройку модельного сериалайзера мы провели:


  • указали, что одноимённое с полем модели поле name работает только на запись. В данных, получаемых из базы, ключа name не будет;
  • добавили сериалайзеру ещё одно поле под названием town и установили, что оно:
    • работает только на чтение, потому что только при получении словаря с данными о записи в модели Town там будет ключ town;
    • источником (source) значения для этого ключа будет атрибут (поле) name записи в модели Town.

Отмечу, что read_only_fields можно задать и в качестве отдельного атрибута внутри Meta, но обязательно в виде списка или кортежа.


Пример показывает, насколько гибким может быть сериалайзер и что не следует ограничивать осмысление DRF схемой «количество полей сериалайзера = количество полей модели». С одним и тем же полем модели может работать несколько полей сериалайзера и даже несколько разных сериалайзеров.


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


  • атрибуты required, default, allow_blank, min_length, max_length, min_value, max_value, validators, querysetс атрибутом read_only=True. DRF обрежет эти атрибуты, оставив только read_only;
  • атрибут required с атрибутом default, в котором есть какое-либо truthy-значение. DRF удалит required, оставив default.

Особенности валидации в ModelSerializer


Метавалидаторы unique_together, unique_for_date, unique_for_month, unique_for_year. При наличии таких валидаторов в модели DRF автоматически перенесёт их в сериалайзер. За это отвечает метод get_validators. Но здесь есть два подводных камня.


  1. Никакого автоматического переноса не будет, если во внутреннем классе Meta задан атрибут validators.
    Иными словами, нельзя полагаться на то, что можно указать в validators: например, если указать кастомный метавалидатор, к нему автоматом не подтянутся рассматриваемые метавалидаторы из модели.
    Если вы решили использовать атрибут validators, значит, нужно указывать в нём все метавалидаторы, включая те, которые уже есть в модели.
  2. unique_together в настоящее время не рекомендован для использования в джанго-моделях. Вместо него документация советует использовать опцию constraints и класс UniqueConstraint.
    Если вы следуете рекомендации, то соответствующий валидатор нужно перенести в сериалайзер вручную, потому что DRF автоматически этого не сделает.

Валидаторы на уровне поля. Валидаторы, как из параметра validators, так и из специальных параметров, за которыми стоят различные виды валидаторов, например аргументы unique, max_value, min_value, автоматически переносятся в поля сериалайзера. За это отвечает метод get_field_kwargs из restframework.utils.field_mapping.


Важно: сказанное относится только к автоматически создаваемым полям. Для декларируемых полей все валидаторы нужно указывать вручную.




На этом я завершаю цикл статей о том, как устроены сериалайзеры в Django REST Framework. Надеюсь, что этот материал позволит заложить крепкий фундамент в понимании одной из важнейших частей фреймворка и поможет создавать отлично работающие API в ваших проектах.




Все статьи серии:
Django Rest Framework для начинающих: создаём API для чтения данных: часть 1 и часть 2
Django Rest Framework для начинающих: создаём API для записи и обновления данных: часть 1 и часть 2

Источник: https://habr.com/ru/company/yandex_praktikum/blog/598349/


Интересные статьи

Интересные статьи

В прошлой статье мы рассказывали, как у нас в банке работает платформа WSO2. Мы предоставляем ее как сервис, как интеграционный слой, следим за его стабильностью, а разработкой на платформе занимаются...
Меня зовут Абденассер, и сегодня я покажу, как пользоваться небольшой scaffold-библиотекой для создания готовых к использованию полнофункциональных REST API на Django за секунды из командной строки, к...
Однажды мы поняли, что для качественной и быстрой реализации разносторонних требований пользователей нам срочно нужны плагины. Изучив разнообразие имеющихся платформ для ...
В предыдущей статье мы разработали простое защищенное веб приложение, в котором для аутентификации пользователей использовался протокол OAuth2 с Bitbucket в качестве сервера авторизации...
Здравствуйте. Не так давно Ericsson выпустил новую систему управления Ericsson Network Manager (ENM), которая уже успела появится у некоторых операторов сотовой связи. Было бы интересно разобрать...