В прошлой части мы в общих чертах рассмотрели, как устроен 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.
@classmethod def many_init(cls, *args, **kwargs): ... child_serializer = cls(*args, **kwargs) list_kwargs = { 'child': child_serializer, } ... meta = getattr(cls, 'Meta', None) list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer) return list_serializer_class(*args, **list_kwargs)
Экземпляр сериалайзера | Описание | К какому классу относится |
---|---|---|
serializer_for_queryset |
Обрабатывает набор табличных записей | ListSerializer класс из модуля
restframework.serializers |
serializer_for_queryset.child |
Обрабатывает каждую отдельную запись в наборе | CapitalSerializer наш собственный класс,
наследует от класса Serializer модуля
restframework.serializers |
Помимо many=True
мы передали значение для атрибута
instance (инстанс). В нём набор записей из модели.
Важное замечание: чтобы не запутаться и
понимать, когда речь идёт о сериалайзере в целом, а когда о
дочернем сериалайзере, далее по тексту мы будем говорить основной
сериалайзер (в
коде контроллера это serializer_for_queryset
) и
дочерний сериалайзер (атрибут child
основного
сериалайзера).
После создания основного сериалайзера мы обращаемся к его
атрибуту data
:
return Response(serializer_for_queryset.data)
Запускается целый набор операций, каждую из которых подробно рассмотрим далее.
Что
под капотом атрибута data
основного сериалайзера
Важное замечание: атрибут data
есть и у основного, и у дочернего сериалайзеров. Поэтому, чтобы
найти подходящий исходный код, нужно помнить: экземпляр основного
(serializer_for_queryset
) относится к классу
ListSerializer
.
Исходный код атрибута data
:
class ListSerializer(BaseSerializer): ... @property def data(self): ret = super().data return ReturnList(ret, serializer=self)
Задействован атрибут data
родительского
BaseSerializer
. Исходный код:
class BaseSerializer(Field): @property def data(self): ... if not hasattr(self, '_data'): if self.instance is not None and not getattr(self, '_errors', None): self._data = self.to_representation(self.instance) ... return self._data
Поскольку никакие данные ещё не сгенерированы (нет атрибута
_data
), ничего не валидируется (нет
_errors
), но есть инстанс (набор записей для
сериализации), запускается метод to_representation
,
который и обрабатывает набор записей из модели.
Как
работает метод to_represantation
основного
сериалайзера
Возвращаемся в класс ListSerializer
.
class ListSerializer(BaseSerializer): def to_representation(self, data): """ List of object instances -> List of dicts of primitive datatypes. """ iterable = data.all() if isinstance(data, models.Manager) else data return [ self.child.to_representation(item) for item in iterable ]
Код нехитрый:
- набор записей из модели (его передавали при создании
сериалайзера в аргументе
instance
) помещается в цикл в качестве единственного аргументаdata
; - в ходе работы цикла каждая запись из набора обрабатывается
методом
to_representation
дочернего сериалайзера (self.child.to_representation(item)
). Теперь понятно, зачем нужна конструкция основной дочерний сериалайзер.
Сделаем небольшую остановку:
- Чтобы обрабатывать не одну запись из БД, а набор, при создании
сериалайзера нужно указать
many=True
. - В этом случае мы получим матрёшку основной сериалайзер с дочерним внутри.
- Задача основного сериалайзера (он относится к классу
ListSerializer
) запустить цикл, в ходе которого дочерний обработает каждую запись и превратит ее в словарь.
Как
работает метод to_representation
дочернего
сериалайзера
Дочерний сериалайзер экземпляр класса
CapitalSerializer
наследует от
restframework.serializers.Serializer
.
class Serializer(BaseSerializer, metaclass=SerializerMetaclass): def to_representation(self, instance): """ Object instance -> Dict of primitive datatypes. """ ret = OrderedDict() fields = self._readable_fields for field in fields: try: attribute = field.get_attribute(instance) except SkipField: continue check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute if check_for_none is None: ret[field.field_name] = None else: ret[field.field_name] = field.to_representation(attribute) return ret
Пойдём по порядку: сначала создаётся пустой
OrderedDict
, далее идёт обращение к атрибуту
_readable_fields
.
Откуда берётся _readable_fields
? Смотрим
исходный код:
class Serializer(BaseSerializer, metaclass=SerializerMetaclass): @property def _readable_fields(self): for field in self.fields.values(): if not field.write_only: yield field
То есть _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
основного
сериалайзера:
[self.child.to_representation(item) for item in iterable]
Этот item (отдельная запись из набора) и есть инстанс, с которым
работает метод get_attribute
конкретного поля.
class Field: ... def get_attribute(self, instance): try: return get_attribute(instance, self.source_attrs) ...
Вызывается функция get_attribute
, описанная на
уровне всего модуля rest_framework.fields
. Функция
получает на вход запись из модели и значение атрибута поля
source_attrs
. Это
список, который возникает в результате применения метода
split
(разделитель точка) к строке, которая
передавалась в аргументе source
при создании поля.
Если такой аргумент не передавали, то в качестве
source
будет взято
имя поля.
Если вспомнить, как работает строковый метод
split
, станет понятно, что если при указании
source не применялась точечная нотация, то список всегда будет из
одного элемента.
У нас есть такие поля:
class CapitalSerializer(serializers.Serializer): capital_city = serializers.CharField(max_length=200) capital_population = serializers.IntegerField() author = serializers.CharField(source='author.username', max_length=200)
Получается следующая картина:
Поле сериалайзера | Значение атрибута 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.fields
:
def get_attribute(instance, attrs): for attr in attrs: try: if isinstance(instance, Mapping): instance = instance[attr] else: instance = getattr(instance, attr) ... return instance
Для полей capital_city
и
capital_population
цикл for attr in attrs
отработает однократно и выполнит инструкцию instance =
getattr(instance, attr)
. Встроенная Python-функция
getattr
извлекает из объекта записи (instance)
значение, присвоенное конкретному атрибуту (attr
)
этого объекта.
При обработке записей из нашей таблицы рассматриваемую строку
исходного кода можно представить примерно так:
instance = getattr(запись_о_конкретной_столице, 'capital_city')
С author.username
ситуация интереснее. До значения
атрибута username
DRF будет добираться так:
- На первой итерации инстанс это объект записи из модели Capital.
Из
source_attrs
берётся первый элементauthor
, и значение одноимённого атрибута становится новым инстансом.author
объект из модели User, с которой Capital связана через внешний ключ. - На следующей итерации из
source_attrs
берётся второй элементusername
. Значение атрибутаusername
будет взято уже от нового инстанса объектаauthor
. Так мы и получаем имя автора.
Извлечённые из объекта табличной записи данные помещаются в
упорядоченный словарь ret
, но перед этим с ними
работает метод to_representation
поля
сериалайзера:
ret[field.field_name] = field.to_representation(attribute)
Задача метода to_representation
представить
извлечённые из записи данные в определённом виде. Например, если
поле сериалайзера относится к классу
CharField
, то извлечённые данные будут приведены к
строке, а если
IntegerField
к целому числу.
В нашем случае применение to_representation
по сути
ничего не даст. Например, из поля табличной записи
capital_city
будет извлечена строка. Метод
to_representation
поля CharField
к
извлечённой строке применит метод str
. Очевидно, что
строка останется строкой, то есть какого-то реального
преобразования не произойдёт. Но если бы из поля табличной записи
IntegerField
извлекались целые числа и передавались
полю класса CharField
, то в итоге они превращались бы
в строки.
При необходимости создать собственный класс поля сериалайзера,
описать специфичную логику и для метода get_attribute
,
и для метода to_representation
, чтобы как угодно
преобразовывать поступившие на сериализацию данные.
Примеры есть в документации кастомные классы
ColorField
и ClassNameField
.
Суммируем всё, что узнали
Преобразованный набор записей из Django-модели доступен в
атрибуте data
основного сериалайзера. При обращении к
этому атрибуту задействуются следующие методы и атрибуты из-под
капота DRF (разумеется, эти методы можно переопределить):
Метод, атрибут, функция | Класс, модуль | Действие |
---|---|---|
data |
serializes.BaseSerializer |
Запускает метод to_representation основного
сериалайзера. |
to_representation |
serializers.ListSerializer |
Запускает цикл, в ходе которого к каждой записи из набора
применяется метод to_representation дочернего
сериалайзера. |
to_representation |
serializers.Serializer |
Сначала создаётся экземпляр упорядоченного словаря, пока он
пустой. Далее запускается цикл по всем полям сериалайзера, у
которых не выставлено write_only=True . |
get_attribute |
fields (вызывается методом
get_attribute класса
fields.Field ) |
Функция стыкует поле сериалайзера с полем записи из БД. По
умолчанию идет поиск поля, чьё название совпадает с названием поля
сериалайзера. Если передавался аргумент source ,
сопоставление будет идти со значением этого аргумента. Из
найденного поля табличной записи извлекается значение текст, числа
и т.д. |
to_representation |
fields.КлассПоляКонкретногоТипа |
Извлечённое значение преобразуется согласно логике
рассматриваемого метода. У каждого поля restframework она своя.
Можно создать собственный класс поля и наделить его метод
to_representation любой нужной логикой. |
В словарь заносится пара ключ-значение:
- ключ название поля сериалайзера;
- значение данные, возвращённые методом
to_representation
поля сериалайзера.
Итог: список из OrderedDict
в количестве, равном
числу переданных и сериализованных записей из модели.
Надеюсь, статья оказалась полезной и позволила дать картину того, как под капотом DRF происходит сериализация данных из БД. Если у вас остались вопросы, задавайте их в комментариях разберёмся вместе.