В прошлой части мы в общих чертах рассмотрели, как устроен 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 |
serializers.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 происходит сериализация данных из БД. Если у вас остались вопросы, задавайте их в комментариях — разберёмся вместе.