16.07.2021       Выпуск 395 (12.07.2021 - 18.07.2021)       Статьи

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

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

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

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

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

Продолжаем изучать Django Rest Framework с точки зрения новичка. Мы уже разобрали создание

REST API для получения данных

из БД, включая

отдельную статью

о работе сериалайзера.

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

image

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) экземпляра сериалайзера нужно подобрать правильный набор аргументов. Выбор зависит от того, какие запросы будут обрабатываться.

Пример:

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

.

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

Примеры:

{'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е

. Затем сериалайзер

перебирает каждое поле в цикле

.

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

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

метод validate_empty_values

класса

fields.Field

и

проверяет

допустимость значения

empty

.

empty

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

empty

вызвана тем, что

None

вполне может быть валидным значением.

Примечание:

результат, попадает или не попадает поле в

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

Метод

serializers.Serializer.to_internal_value

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

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

При работе с

ModelSerializer

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

ModelSerializer

.


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

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






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus