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

Продолжаем изучать Django Rest Framework с точки зрения новичка. Мы уже разобрали создание REST API для получения данных из БД, включая отдельную статью о работе сериалайзера.

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

DRF позволяет не только извлекать и передавать записи из БД сторонним приложениям, но и принимать от них данные для использования на вашем веб-сайте. Например, чтобы создать новую запись в БД или обновить существующую. Когда REST API принимает данные извне, происходит их десериализация ― восстановление Python-объекта из последовательности байтов, пришедших по сети.

Процесс создания или обновления одной записи в БД с помощью DRF включает следующие шаги:

1. Объявляем класс сериалайзера, через который будут проходить входные данные. Один и тот же класс сериалайзера может работать одновременно и на запись, и на чтение.

2. Стороннее приложение отправляет POST-, PUT- или PATCH-запрос к эндпоинту API.

3. Контроллер (view), отвечающий за эндпоинт, извлекает из атрибута data объекта request данные для записи.

4. В контроллере создаём экземпляр сериалайзера, которому передаём поступившие данные, а также при необходимости запись из БД, которую предстоит обновить, и другие аргументы.

5. Вызываем метод is_valid сериалайзера. Он валидирует данные, а также позволяет скорректировать и расширить их. При валидации используются как инструменты из-под капота, так и наши собственные методы.

6. При успешной валидации вызываем метод save сериалайзера, благодаря которому в БД создаётся новая запись или обновляется существующая.

Одной статьи для подробного разбора, увы, не хватит, поэтому я снова разделил её на две части. В первой части поговорим о создании и работе сериалайзера на запись — это шаги 1, 3 и 5. В следующей статье рассмотрим остальные шаги и проиллюстрируем работу API на примерах.

Важно: как и в случае с сериалайзером на чтение, рассмотрим работу сериалайзера на запись на основе класса serializers.Serializer. Об особенностях работы дочернего класса ModelSerializer поговорим в отдельной статье.

Объявляем класс сериалайзера на запись

Чтобы сериалайзер мог работать на запись, у него должны быть:

  • поля, которые могут работать на запись, — поля с атрибутом read_only=True будут игнорироваться;
  • методы create (если хотим сохранить в БД новую запись) и update (если хотим обновить существующую запись).

Напомню, что один и тот же класс сериалайзера может работать одновременно и на запись, и на чтение. Можно сделать и разные сериалайзеры под разные запросы.

Попробую пояснить на примере из документации:

from rest_framework import serializers
 
class BlogPostSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=100)
    content = serializers.CharField(source="text")

Сериалайзер может работать на чтение, преобразовывая каждую переданную из БД запись, у которой есть атрибуты title и text, в словарь {‘title’: ‘значение’, ‘content’: ‘значение’}. Если атрибутов title или text у записи не окажется, возникнет исключение.

Этот же сериалайзер может работать на запись — только нужно дописать методы create и update. Тогда на вход он будет ожидать словарь {‘title’: ‘значение’, ‘content’: ‘значение’}. Если таких ключей в словаре не окажется, по ним будут пустые значения или по ключу title будет строка длиной более 100 символов — снова появится исключение. При штатной отработке вернётся словарь с проверенными данными. Причём один ключ будет title, а вот второй — text. На это поведение влияет именованный аргумент source.

Если такой объём и формат исходящих/входящих данных вас устраивает, можно оставить один класс. Более развёрнутые примеры классов сериалайзера на запись я приведу в следующей статье.

Создаём экземпляр сериалайзера на запись

При создании в контроллере (view) экземпляра сериалайзера нужно подобрать правильный набор аргументов. Выбор зависит от того, какие запросы будут обрабатываться.

Аргумент «На чтение» — обработка одной записи из БД или их набора для выдачи по GET-запросу «На запись» — создать новую запись в БД по POST-запросу «На запись» — обновить конкретную запись в БД по PUT- или PATCH-запросу
instance Одна или несколько записей из БД Не передаём Передаём запись из БД, которую собираемся обновить
data Не передаём Словарь с данными, которые хотим валидировать и сохранить в БД. Если many=True, то передаём список словарей Словарь с данными для полного или частичного обновления существующей в БД записи. Если many=True, то передаём список словарей
many Передаём со значением True, если из БД извлекаем не одну, а несколько записей Передаём со значением True, если на вход поступают данные не для одной, а для нескольких будущих записей в БД Передаём со значением True, если хотим частично или полностью обновить сразу несколько записей в БД
partial Не передаём Не передаём Передаём со значением True для PATCH-запросов
context Через этот аргумент можем передать любые данные, которые нужны сериалайзеру

Пример:

serializer = SerializerForUpdateData(
    instance=current_entry_in_db,
    data=input_data,
    partial=True
)

Такие аргументы говорят нам, что экземпляр сериалайзера создан для частичного обновления существующей записи в БД.

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

Валидируем с помощью сериалайзера входные данные

Это ключевая задача сериалайзера при работе на запись, поэтому уделим ей максимальное внимание.

Валидацию запускает метод is_valid. Итог его работы ― два новых атрибута сериалайзера: validated_data и errors.

В каждом атрибуте ― словарь, причём один из них всегда пустой. Если ошибок нет, пусто в errors, а если есть ― в validated_data. В первом случае is_valid возвращает True, во втором False.

Рассмотрим, из чего состоят пары «ключ–значение» в этих словарях.

Словарь Ключи Значения
validated_data названия полей сериалайзера значения из поступившего в аргументе data словаря по ключам, имя которых идентично именам полей сериалайзера, а также дефолтные значения полей сериалайзера (если входных данных нет)
errors название полей сериалайзера либо non_field_errors расшифровки ошибок, которые возникли при валидации полей, либо ошибки, возникшей при валидации вне конкретного поля

Примеры:

{'capital_city': 'London'}

В поступившем в data словаре по ключу capital_city есть значение ‘London’. Оно успешно валидировано через поле capital_city сериалайзера.

{'non_field_errors': [ErrorDetail(string='Invalid data. Expected a dictionary, but got str.', code="invalid")]}.

На вход в аргументе data сериалайзер ожидает словарь, но пришла строка.

{'non_field_errors': [ErrorDetail(string='The fields country, capital_city must make a unique set.', code="unique")]}.

Пара значений по ключам capital_city и country не должны повторять идентичное сочетание значений в таблице в БД.

{'capital_city': [ErrorDetail(string='This field is required.', code="required")]}.

В поступившем на вход словаре по ключу capital_city — пустая строка. Значение не прошло валидацию в поле capital_city сериалайзера, поскольку поле требует непустых значений.

Совпадение имён ключей в словаре, который поступает в сериалайзер, с именами полей сериалайзера ― принципиальная вещь. Если будут нестыковки, велика вероятность получить ошибку сразу или чуть позже, когда выяснится, что для записи в БД не хватает части данных, которые были на входе в сериалайзер.

У метода is_valid есть один аргумент ― raise_exception. Если у него значение False, которое задано по умолчанию, метод не будет выбрасывать ValidationError. Даже если будут ошибки, метод отработает до конца, вернёт False, а информация об ошибках будет доступна в атрибуте errors. На ошибки любых иных типов настройка raise_exception не влияет.

Как происходит валидация после запуска is_valid

Валидация носит многоступенчатый характер и условно её можно разделить на три этапа:

  1. Проверка, есть ли что валидировать.
  2. Проверки поступивших данных на уровне полей сериалайзера.
  3. Проверки на метауровне, когда можно проверить поступившие данные не для конкретного поля, а целиком.

Важно: ниже описывается процесс валидации данных, которые предназначены для одной записи в БД. Как и в случае с сериалайзером на чтение, на запись можно выставить many=True и принимать набор данных. Тогда появится ещё одна ступень проверки ― на входе будет ожидаться список словарей, а не один словарь. Далее по этому списку запускается цикл, и каждый отдельный словарь с данными будет проверяться так же, как описано ниже.

Этап 1. Есть ли что валидировать

DRF проверяет, есть ли у сериалайзера атрибут initial_data. Этот атрибут создаётся, если при создании сериалайзера был передан аргумент data. Если его нет, то будет выброшено исключение AssertionError.

Далее идёт проверка содержимого и формата data.

Если в data ничего не оказалось (None), то возможны два исхода:

  • ValidationError;
  • окончание валидации с возвратом None в validated_data, если при создании сериалайзера передавали аргумент allow_null со значением True.

Если data всё же что-то содержит, DRF проверяет тип поступивших данных — они должны быть словарём.

Этап 2. Проверки на уровне полей

Важнейшие тезисы:

  • если для конкретного ключа из поступившего словаря нет одноимённого writable-поля сериалайзера, пара «ключ–значение» останется за бортом валидации;
  • если для конкретного writable-поля сериалайзера не окажется одноимённого ключа в поступившем словаре или ключ будет, но его значение None, может быть несколько вариантов развития событий. Либо поднимется исключение, либо продолжится валидация значения поля, либо поле будет проигнорировано;
  • проверку, уже встроенную в класс конкретного поля, можно усилить валидаторами, а также описав собственный метод validate_названиеПоля;
  • проверки идут последовательно по всем полям, и только после этого запускается следующий этап ― проверки на метауровне.

В методе to_internal_value сериалайзер собирает в генератор все поля, которые могут работать на запись, то есть те поля, у которых нет read_only=Truе. Затем сериалайзер перебирает каждое поле в цикле.

Проверка Действие Результат
Есть ли у поля кастомный метод валидации? Для каждого поля внутри класса сериалайзера можно описать проверку значения с любой нужной логикой. Название метода должно быть в формате validate_НазваниеПоля.
Исходный код
Если метод есть, он будет задействован позднее.
Есть ли в поступившем словаре ключ с тем же именем, что и поле сериалайзера? Задача ― извлечь значение для дальнейшей валидации.
Исходный код
Если ключ найден, для дальнейшей валидации берётся его значение, если не найден ― значение empty.

Этап 2.1. Валидирование отсутствующих значений для поля

Если для поля не нашлось одноимённого ключа в поступившем словаре, срабатывает метод validate_empty_values класса fields.Field и проверяет допустимость значения empty.

empty ― это просто пустой класс. DRF передаёт его в качестве значения полям, для которых не нашлось значений во входных данных. Он помечает эти поля как пустые и валидирует определённым образом. Как поясняют разработчики DRF, необходимость в empty вызвана тем, что None вполне может быть валидным значением.

Поле обязательно для заполнения (required=True) Сериалайзер допускает частичное обновление (partial=True) У поля есть дефолтное значение (default=…) Результат
+ Не имеет значения
(и не может быть, если required=True)
ValidationError
Не имеет значения Поднимается исключение SkipField, поле дальше не валидируется и не попадает в validated_data
+ + Поле дальше не валидируется и не попадает в validated_data
+ Поле валидируется дальше и вместе с дефолтным значением попадает в validated_data

Примечание: результат, попадает или не попадает поле в validated_data, указан с условием того, что остальные поля успешно прошли валидацию. Если хотя бы одно поле провалит проверку, словарь validated_data всегда будет пустым.

Если в поле значение None, работает метод validate_empty_values класса fields.Field.

В этом случае проверяется, есть ли у поля атрибут allow_null в значении True. Если его нет, появится ValidationError. Если allow_null=True, дальнейшая валидация внутри поля прекратится. Если значение None пройдёт проверку вне поля (метавалидаторами), то это значение и войдёт в validated_data.

После проверок на empty и None запускаются проверочные механизмы внутри конкретного поля.

Этап 2.2. Проверка в поле методом to_internal_value

Важно: если значение empty или None, проверка не проводится.

У каждого поля DRF, которое может работать на запись, есть метод to_internal_value. Чтобы понять логику этого метода, нужно заглянуть под капот в класс соответствующего поля.

Приведу примерto_internal_value поля класса CharField.

    def to_internal_value(self, data):
        if isinstance(data, bool) or not isinstance(data, (str, int, float,)):
            self.fail('invalid')
        value = str(data)
        return value.strip() if self.trim_whitespace else value

Проверка выдаст ошибку, если на вход не поступила строка или число. Также не допускаются логические типы True и False. Проверку на наличие последних разработчики выделили отдельно, т. к. класс bool наследует от класса int.

Если вы собираетесь использовать кастомный класс поля для валидации входных данных, проследите, чтобы там был метод to_internal_value, иначе DRF укажет на ошибку.

Этап 2.3. Проверка поля валидаторами

Важно: проверка не проводится, если значение empty или None.

При объявлении поля сериалайзера среди аргументов можно указать:

Валидаторы передаются списком в аргументе validators при описании поля сериалайзера, даже если валидатор всего один. Некоторые валидаторы можно передать через специально предусмотренные атрибуты конкретного поля. Например, у поля сериалайзера IntegerField есть аргумент max_value, который создаёт джанго-валидатор MaxValueValidator. Поэтому оба варианта будут верны, но в первом случае нужно ещё сделать импорт из django.core.validators:

capital_population = serializers.IntegerField(
    validators=[MaxValueValidator(1000000)]
)

capital_population = serializers.IntegerField(
    max_value=1000000
)

Также отмечу, что некоторые поля могут наделяться валидаторами из-под капота без необходимости объявлять их явно. Например, в поле CharField уже заложены два валидатора, названия которых говорят сами за себя: джанго-валидатор ProhibitNullCharactersValidator и DRF-валидатор ProhibitSurrogateCharactersValidator.

Этап 2.4. Проверка кастомным методом validate_названиеПоля

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

Логику задаём любую. Результат работы метода ― возврат значения или ошибки. Скелет метода можно представить так:

def validate_НазваниеПоляСериалайзера(self, value):
    if условия_при_которых_значение_невалидно:
        raise serializers.ValidationError("Описание ошибки")
    return value

Обратите внимание на self — за ним стоит экземпляр сериалайзера. Через него есть доступ к различной ценной информации, которая может пригодиться при валидации. Например, через self.initial_data можно получить доступ ко всему словарю с входными данными до начала их валидации.

И ещё один момент, который следует держать в голове при описании логики метода: если допускается, что поле будет пустым, и есть дефолтное значение, а также если в поле можно передавать None, эти значения также будут поступать в рассматриваемый метод.

Этап 2.5. Присвоение имени ключу с успешно валидированным в поле значением

В случае успеха метод to_internal_value сериалайзера возвращает словарь с проверенными данными. По умолчанию именем ключа становится имя поля сериалайзера. Но это поведение можно переопределить благодаря атрибуту source, о котором мы подробно говорили в статьях о работе сериалайзера на чтение.

Если у поля есть атрибут source, то именем ключа станет не имя соответствующего поля, а значение из атрибута source. Такая логика описана в функции set_values модуля restframework.fields. Эта функция вызывается в конце работы to_internal_value и получает в качестве аргумента keys атрибут source_attrs поля (мы подробно разбирали его в предыдущей статье).

Обратимся к примеру.

content = serializers.CharField(source="text")

Если это поле используется при работе на запись, то сериалайзер будет искать во входных данных ключ content и валидировать значение по этому ключу методом to_internal_value. В случае успеха он вернёт ― внимание! ― валидированное значение уже с ключом ‘text’. Получится ‘text’: ‘валидированное значение, которое пришло с ключом content’. Именно в таком виде пара «ключ–значение» попадут в validated_data, но только если пройдут следующий этап ― проверку метавалидаторами.

Этап 3. Проверка на уровне всего сериалайзера

Этап разбивается на две части.

Этап 3.1. Проверка метавалидаторами
Эти валидаторы не привязаны к конкретному полю и получают на вход весь набор данных, которые прошли проверку в полях.

Чтобы задать метавалидатор, нужно прописать внутри класса нашего сериалайзера класс Meta с атрибутом validators. Как и валидаторы на уровне полей, метавалидаторы указывают списком, даже если валидатор один.

Пример метавалидатора из коробки ― UniqueTogetherValidator. Он проверяет, уникально ли сочетание значений из нескольких полей по сравнению с тем, что уже есть в БД.

Этап 3.2. Проверка методом validate
Последний рубеж валидации так же, как и метавалидаторы, опционален. Заготовка метода validate уже находится под капотом родительского класса сериалайзера.

    def validate(self, attrs):
        return attrs

Если в нём есть необходимость, достаточно переопределить метод.

Метод validate, как и метавалидаторы, на вход принимает весь набор валидированных данных и позволяет сверить их между собой и с данными в БД в одном месте.

Для закрепления: таблица с последовательностью валидирования входных данных в DRF

Этап Что проверяется Метод Примечание
1 Передан ли аргумент data при создании сериалайзера serializers.BaseSerializer.
is_valid
2 Если в data сериалайзеру передано None, допустимо ли это fields.Field.
validate_empty_values
Если передано None, валидация завершается
3 Передан ли в data словарь serializers.Serializer.
to_internal_value

Метод serializers.Serializer.to_internal_value запускает цикл по всем writable-полям со следующими проверками по каждому полю:

Этап Что проверяется Метод Примечание
4 Есть ли в data ключ с таким же именем, что и поле сериалайзера fields.Field.
get_value
Если ключа нет или по нему нет значения и это допустимо, значением поля становится класс empty
5 Если значение поля empty, есть ли у поля дефолтное значение fields.Field.
get_default
Если дефолтного значения нет, поле исключается из валидации. В validated_data оно никак не будет представлено
6 Если значение из входных данных None, допускает ли поле это fields.Field.
validate_empty_values
Допустимость определяется значением атрибута allow_null поля
7 Соответствует ли значение внутренним требованиям поля fields.Field.
классКонкретногоПоля.
to_internal_value
Проверка не проводится, если значение empty или None
8 Проходят ли значение валидаторы, которые встроены в поле или приданы ему fields.Field.run_validators Не предусмотренные изначально валидаторы нужно установить самостоятельно.
Проверка не проводится, если значение empty или None
9 Проходит ли значение поля проверку кастомным методом валидации метод validate_НазваниеПоля класса сериалайзера Логику метода нужно описать самостоятельно

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

Этап Что проверяется Метод Примечание
10 Проходят ли значения полей метавалидаторы атрибут validators класса Meta сериалайзера Для проверки валидаторы нужно задать самостоятельно
11 Проходят ли все значения полей вместе проверку кастомным методом сериалайзера метод validate сериалайзера Для проверки логику метода нужно описать самостоятельно

При работе с ModelSerializer валидаторы из модели могут автоматически переноситься в сериалайзер, поэтому вручную их можно не указывать. Об этом мы поговорим в отдельной статье о ModelSerializer.


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

Спасибо за внимание к моей статье. Надеюсь, она помогла сделать ещё один шаг в понимании кухни DRF. И снова жду ваших вопросов в комментариях.

Источник 📢