Русский
Русский
English
Статистика
Реклама

Django Rest Framework для начинающих создаём API для чтения данных (часть 2)

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

Источник: habr.com
К списку статей
Опубликовано: 15.06.2021 12:07:46
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании яндекс.практикум

Яндекс.практикум

Django

Для начинающих

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru