Работа сериалайзера на чтение

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

Важный момент: мы говорим о работе сериалайзера только на чтение, то есть когда он отдаёт пользователю информацию из базы данных (БД) сайта. О работе на запись, когда данные поступают извне и их надо сохранить в БД, расскажем в следующей статье.

Код учебного проекта, который используется в этой статье, доступен в репозитории на Гитхабеarrow-up-right.

Как создаётся сериалайзер, работающий на чтение

Создание экземпляра сериалайзера мы описывали следующим образом:

# capitals/views.py

        serializer_for_queryset = CapitalSerializer(
            instance=queryset,  # Передаём набор записей
            many=True # Указываем, что на вход подаётся набор записей
        )

Благодаря many=True запускается метод many_init класса BaseSerializer.

class BaseSerializer(Field):

    def __new__(cls, *args, **kwargs):
        if kwargs.pop('many', False):
            return cls.many_init(*args, **kwargs)
        return super().__new__(cls, *args, **kwargs)

Подробнее о методе many_init:

  • При создании экземпляра сериалайзера он меняет родительский класс. Теперь родителем выступает не CapitalSerializer, а класс DRF для обработки наборов записей restframework.serializers.ListSerializer.

  • Созданный экземпляр сериалайзера наделяется атрибутом child. В него включается дочерний сериалайзер — экземпляр класса CapitalSerializer.

Экземпляр сериалайзера
Описание
К какому классу относится

serializer_for_queryset

Обрабатывает набор табличных записей

ListSerializer — класс из модуля restframework.serializers

serializer_for_queryset.child

Обрабатывает каждую отдельную запись в наборе

CapitalSerializer — наш собственный класс, наследует от класса Serializer модуля restframework.serializers

Помимо many=True мы передали значение для атрибута instance (инстанс). В нём — набор записей из модели.

Важное замечание: чтобы не запутаться и понимать, когда речь идёт о сериалайзере в целом, а когда — о дочернем сериалайзере, далее по тексту мы будем говорить «основной сериалайзер» (в коде контроллераarrow-up-right это serializer_for_queryset) и «дочерний сериалайзер» (атрибут child основного сериалайзера).

После создания основного сериалайзера мы обращаемся к его атрибуту data:

Запускается целый набор операций, каждую из которых подробно рассмотрим далее.

Что под капотом атрибута data основного сериалайзера

Важное замечание: атрибут data есть и у основного, и у дочернего сериалайзеров. Поэтому, чтобы найти подходящий исходный код, нужно помнить: экземпляр основного (serializer_for_queryset) относится к классу ListSerializer.

Исходный кодarrow-up-right атрибута data:

Задействован атрибут data родительского BaseSerializer. Исходный кодarrow-up-right:

Поскольку никакие данные ещё не сгенерированы (нет атрибута _data), ничего не валидируется (нет _errors), но есть инстанс (набор записей для сериализации), запускается метод to_representation, который и обрабатывает набор записей из модели.

Как работает метод to_represantation основного сериалайзера

Возвращаемся в класс ListSerializer.

Исходный кодarrow-up-right

Код нехитрый:

  • набор записей из модели (его передавали при создании сериалайзера в аргументе instance) помещается в цикл в качестве единственного аргумента data;

  • в ходе работы цикла каждая запись из набора обрабатывается методом to_representation дочернего сериалайзера (self.child.to_representation(item)). Теперь понятно, зачем нужна конструкция «основной — дочерний сериалайзер».

Сделаем небольшую остановку:

  • Чтобы обрабатывать не одну запись из БД, а набор, при создании сериалайзера нужно указать many=True.

  • В этом случае мы получим матрёшку — основной сериалайзер с дочерним внутри.

  • Задача основного сериалайзера (он относится к классу ListSerializer) — запустить цикл, в ходе которого дочерний обработает каждую запись и превратит ее в словарь.

Как работает метод to_representation дочернего сериалайзера

Дочерний сериалайзер — экземпляр класса CapitalSerializer — наследует от restframework.serializers.Serializer.

Исходный кодarrow-up-right

Пойдём по порядку: сначала создаётся пустой OrderedDict, далее идёт обращение к атрибуту _readable_fields.

Откуда берётся _readable_fields? Смотрим исходный кодarrow-up-right:

То есть _readable_fields — это генератор, включающий поля дочернего сериалайзера, у которых нет атрибутa write_only со значением True. По умолчанию он False. Если объявить True, поле будет работать только на создание или обновление записи, но будет игнорироваться при её представленииarrow-up-right.

В дочернем сериалайзере все поля могут работать на чтение (представление) — ограничений write only не установлено. Это значит, что генератор _readable_fields будет включать три поля — capital_city, capital_population, author.

Читаем код to_representation далее: генератор _readable_fields помещается в цикл, и у каждого поля вызывается метод get_attribute.

Если посмотреть код to_representation дальше, видно, что у поля вызывается и другой метод — to_representation. Это не опечатка: метод to_representation под одним и тем же названием, но с разной логикой:

  • есть у основного сериалайзера в классе ListSerializer;

  • у дочернего сериалайзера в классе Serializer;

  • у каждого поля дочернего сериалайзера в классе соответствующего поля.

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

Как запись из модели обрабатывается методами полей сериалайзера

Метод get_attribute работает с инстансом (instance). Важно не путать этот инстанс с инстансом основного сериалайзера. Инстанс основного сериалайзера — это набор записей из модели. Инстанс дочернего сериалайзера — каждая конкретная запись.

Вспомним строкуarrow-up-right из кода to_representation основного сериалайзера:

Этот item (отдельная запись из набора) и есть инстанс, с которым работает метод get_attribute конкретного поля.

Исходный кодarrow-up-right

Вызывается функция get_attribute, описанная на уровне всего модуля rest_framework.fields. Функция получает на вход запись из модели и значение атрибута поля source_attrs. Это списокarrow-up-right, который возникает в результате применения метода split (разделитель — точка) к строке, которая передавалась в аргументе source при создании поля. Если такой аргумент не передавали, то в качестве source будет взято имя поляarrow-up-right.

Если вспомнить, как работает строковый метод splitarrow-up-right, станет понятно, что если при указании source не применялась точечная нотация, то список всегда будет из одного элемента.

У нас есть такие поля:

Получается следующая картина:

Поле сериалайзера
Значение атрибута source поля
Значение source_attrs

capital_city

'capital_city'

['capital_city']

capital_population

'capital_population'

['capital_population']

author

'author.username'

['author', 'username']

Как мы уже указывали, список source_attrs в качестве аргумента attrs передаётся в метод get_attribute rest_framework.fieldsarrow-up-right:

Для полей capital_city и capital_population цикл for attr in attrs отработает однократно и выполнит инструкцию instance = getattr(instance, attr). Встроенная Python-функция getattrarrow-up-right извлекает из объекта записи (instance) значение, присвоенное конкретному атрибуту (attr) этого объекта. При обработке записей из нашей таблицы рассматриваемую строку исходного кода можно представить примерно так:

С author.username ситуация интереснее. До значения атрибута username DRF будет добираться так:

  • На первой итерации инстанс — это объект записи из модели Capital. Из source_attrs берётся первый элемент author, и значение одноимённого атрибута становится новым инстансом. author — объект из модели User, с которой Capital связана через внешний ключ.

  • На следующей итерации из source_attrs берётся второй элемент username. Значение атрибута username будет взято уже от нового инстанса — объекта author. Так мы и получаем имя автора.

Извлечённые из объекта табличной записи данные помещаются в упорядоченный словарь ret, но перед этим с ними работает метод to_representation поля сериалайзера:

Задача метода to_representation — представить извлечённые из записи данные в определённом виде. Например, если поле сериалайзера относится к классу CharFieldarrow-up-right, то извлечённые данные будут приведены к строке, а если IntegerFieldarrow-up-right — к целому числу.

В нашем случае применение to_representation по сути ничего не даст. Например, из поля табличной записи capital_city будет извлечена строка. Метод to_representation поля CharField к извлечённой строке применит метод str. Очевидно, что строка останется строкой, то есть какого-то реального преобразования не произойдёт. Но если бы из поля табличной записи IntegerField извлекались целые числа и передавались полю класса CharField, то в итоге они превращались бы в строки.

При необходимости можно создать собственный класс поля сериалайзера, описать специфичную логику и для метода get_attribute, и для метода to_representation, чтобы как угодно преобразовывать поступившие на сериализацию данные. Примерыarrow-up-right есть в документации — кастомные классы ColorField и ClassNameField.

Суммируем всё, что узнали

Преобразованный набор записей из Django-модели доступен в атрибуте data основного сериалайзера. При обращении к этому атрибуту задействуются следующие методы и атрибуты из-под капота DRF (разумеется, эти методы можно переопределить):

Метод, атрибут, функция
Класс, модуль
Действие

serializers.BaseSerializer

Запускает метод to_representation основного сериалайзера.

serializers.ListSerializer

Запускает цикл, в ходе которого к каждой записи из набора применяется метод to_representation дочернего сериалайзера.

serializers.Serializer

Сначала создаётся экземпляр упорядоченного словаря, пока он пустой. Далее запускается цикл по всем полям сериалайзера, у которых не выставлено write_only=True.

fields (вызывается методом get_attributearrow-up-right класса fields.Field)

Функция стыкует поле сериалайзера с полем записи из БД. По умолчанию идет поиск поля, чьё название совпадает с названием поля сериалайзера. Если передавался аргумент source, сопоставление будет идти со значением этого аргумента. Из найденного поля табличной записи извлекается значение — текст, числа и т.д.

to_representation

fields.КлассПоляКонкретногоТипа

Извлечённое значение преобразуется согласно логике рассматриваемого метода. У каждого поля restframework она своя. Можно создать собственный класс поля и наделить его метод to_representation любой нужной логикой.

В словарь заносится пара «ключ-значение»:

  • ключ — название поля сериалайзера;

  • значение — данные, возвращённые методом to_representation поля сериалайзера.

Итог: список из OrderedDict в количестве, равном числу переданных и сериализованных записей из модели.


Last updated