Работа сериалайзера на чтение
В прошлой части мы в общих чертах рассмотрели, как устроен REST API на DRF при работе на чтение. Едва ли не самый сложный для понимания этап — сериализация. Вооружившись исходным кодом, полностью разберем этот этап — от приема набора записей из модели до их преобразования в список словарей.
Важный момент: мы говорим о работе сериалайзера только на чтение, то есть когда он отдаёт пользователю информацию из базы данных (БД) сайта. О работе на запись, когда данные поступают извне и их надо сохранить в БД, расскажем в следующей статье.
Код учебного проекта, который используется в этой статье, доступен в репозитории на Гитхабе.
Как создаётся сериалайзер, работающий на чтение
Создание экземпляра сериалайзера мы описывали следующим образом:
# 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 (инстанс). В нём — набор записей из модели.
Важное замечание: чтобы не запутаться и понимать, когда речь идёт о сериалайзере в целом, а когда — о дочернем сериалайзере, далее по тексту мы будем говорить «основной сериалайзер» (в коде контроллера это serializer_for_queryset) и «дочерний сериалайзер» (атрибут child основного сериалайзера).
После создания основного сериалайзера мы обращаемся к его атрибуту data:
Запускается целый набор операций, каждую из которых подробно рассмотрим далее.
Что под капотом атрибута data основного сериалайзера
data основного сериалайзераВажное замечание: атрибут data есть и у основного, и у дочернего сериалайзеров. Поэтому, чтобы найти подходящий исходный код, нужно помнить: экземпляр основного (serializer_for_queryset) относится к классу ListSerializer.
Исходный код атрибута data:
Задействован атрибут data родительского BaseSerializer. Исходный код:
Поскольку никакие данные ещё не сгенерированы (нет атрибута _data), ничего не валидируется (нет _errors), но есть инстанс (набор записей для сериализации), запускается метод to_representation, который и обрабатывает набор записей из модели.
Как работает метод to_represantation основного сериалайзера
to_represantation основного сериалайзераВозвращаемся в класс ListSerializer.
Код нехитрый:
набор записей из модели (его передавали при создании сериалайзера в аргументе
instance) помещается в цикл в качестве единственного аргументаdata;в ходе работы цикла каждая запись из набора обрабатывается методом
to_representationдочернего сериалайзера (self.child.to_representation(item)). Теперь понятно, зачем нужна конструкция «основной — дочерний сериалайзер».
Сделаем небольшую остановку:
Чтобы обрабатывать не одну запись из БД, а набор, при создании сериалайзера нужно указать
many=True.В этом случае мы получим матрёшку — основной сериалайзер с дочерним внутри.
Задача основного сериалайзера (он относится к классу
ListSerializer) — запустить цикл, в ходе которого дочерний обработает каждую запись и превратит ее в словарь.
Как работает метод to_representation дочернего сериалайзера
to_representation дочернего сериалайзераДочерний сериалайзер — экземпляр класса CapitalSerializer — наследует от restframework.serializers.Serializer.
Пойдём по порядку: сначала создаётся пустой OrderedDict, далее идёт обращение к атрибуту _readable_fields.
Откуда берётся _readable_fields? Смотрим исходный код:
То есть _readable_fields — это генератор, включающий поля дочернего сериалайзера, у которых нет атрибутa write_only со значением True. По умолчанию он False. Если объявить True, поле будет работать только на создание или обновление записи, но будет игнорироваться при её представлении.
В дочернем сериалайзере все поля могут работать на чтение (представление) — ограничений 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). Важно не путать этот инстанс с инстансом основного сериалайзера. Инстанс основного сериалайзера — это набор записей из модели. Инстанс дочернего сериалайзера — каждая конкретная запись.
Вспомним строку из кода to_representation основного сериалайзера:
Этот item (отдельная запись из набора) и есть инстанс, с которым работает метод get_attribute конкретного поля.
Вызывается функция get_attribute, описанная на уровне всего модуля rest_framework.fields. Функция получает на вход запись из модели и значение атрибута поля source_attrs. Это список, который возникает в результате применения метода split (разделитель — точка) к строке, которая передавалась в аргументе source при создании поля. Если такой аргумент не передавали, то в качестве source будет взято имя поля.
Если вспомнить, как работает строковый метод split, станет понятно, что если при указании source не применялась точечная нотация, то список всегда будет из одного элемента.
У нас есть такие поля:
Получается следующая картина:
capital_city
'capital_city'
['capital_city']
capital_population
'capital_population'
['capital_population']
author
'author.username'
['author', 'username']
Как мы уже указывали, список source_attrs в качестве аргумента attrs передаётся в метод get_attribute rest_framework.fields:
Для полей capital_city и capital_population цикл for attr in attrs отработает однократно и выполнит инструкцию instance = getattr(instance, attr). Встроенная Python-функция getattr извлекает из объекта записи (instance) значение, присвоенное конкретному атрибуту (attr) этого объекта. При обработке записей из нашей таблицы рассматриваемую строку исходного кода можно представить примерно так:
С author.username ситуация интереснее. До значения атрибута username DRF будет добираться так:
На первой итерации инстанс — это объект записи из модели Capital. Из
source_attrsберётся первый элементauthor, и значение одноимённого атрибута становится новым инстансом.author— объект из модели User, с которой Capital связана через внешний ключ.На следующей итерации из
source_attrsберётся второй элементusername. Значение атрибутаusernameбудет взято уже от нового инстанса — объектаauthor. Так мы и получаем имя автора.
Извлечённые из объекта табличной записи данные помещаются в упорядоченный словарь ret, но перед этим с ними работает метод to_representation поля сериалайзера:
Задача метода to_representation — представить извлечённые из записи данные в определённом виде. Например, если поле сериалайзера относится к классу CharField, то извлечённые данные будут приведены к строке, а если IntegerField — к целому числу.
В нашем случае применение to_representation по сути ничего не даст. Например, из поля табличной записи capital_city будет извлечена строка. Метод to_representation поля CharField к извлечённой строке применит метод str. Очевидно, что строка останется строкой, то есть какого-то реального преобразования не произойдёт. Но если бы из поля табличной записи IntegerField извлекались целые числа и передавались полю класса CharField, то в итоге они превращались бы в строки.
При необходимости можно создать собственный класс поля сериалайзера, описать специфичную логику и для метода get_attribute, и для метода to_representation, чтобы как угодно преобразовывать поступившие на сериализацию данные. Примеры есть в документации — кастомные классы ColorField и ClassNameField.
Суммируем всё, что узнали
Преобразованный набор записей из Django-модели доступен в атрибуте data основного сериалайзера. При обращении к этому атрибуту задействуются следующие методы и атрибуты из-под капота DRF (разумеется, эти методы можно переопределить):
serializers.ListSerializer
Запускает цикл, в ходе которого к каждой записи из набора применяется метод to_representation дочернего сериалайзера.
serializers.Serializer
Сначала создаётся экземпляр упорядоченного словаря, пока он пустой. Далее запускается цикл по всем полям сериалайзера, у которых не выставлено write_only=True.
fields (вызывается методом get_attribute класса fields.Field)
Функция стыкует поле сериалайзера с полем записи из БД. По умолчанию идет поиск поля, чьё название совпадает с названием поля сериалайзера. Если передавался аргумент source, сопоставление будет идти со значением этого аргумента. Из найденного поля табличной записи извлекается значение — текст, числа и т.д.
to_representation
fields.КлассПоляКонкретногоТипа
Извлечённое значение преобразуется согласно логике рассматриваемого метода. У каждого поля restframework она своя. Можно создать собственный класс поля и наделить его метод to_representation любой нужной логикой.
В словарь заносится пара «ключ-значение»:
ключ — название поля сериалайзера;
значение — данные, возвращённые методом
to_representationполя сериалайзера.
Итог: список из OrderedDict в количестве, равном числу переданных и сериализованных записей из модели.
Last updated