Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Продолжаем цикл статей, посвящённый основам Django Rest Framework. В предыдущей статье мы подробно рассмотрели, как сериалайзер валидирует входные данные.
В этой статье мы закрепим теорию на простом примере, а также затронем те вопросы, которые не успели рассмотреть раньше:
- какое поле сериалайзера выбрать для
ForeignKey
-поля модели; - как сериалайзер работает с датами;
- как устроен метод
save
сериалайзера.
А ещё мы напишем контроллер, который будет обрабатывать запросы к API на создание записи в БД.
Важное замечание: мы по-прежнему работаем с базовым классом сериалайзера, не переходя к более высокому уровню абстракцииModelSerializer
. Это нужно, чтобы глубже понимать принципы работы DRF и при необходимости провести тонкую настройку сериалайзера. ОModelSerializer
мы подробно поговорим в следующей статье.
Исходный код учебного проекта для этой статьи доступен в GitHub.
Объявляем класс сериалайзера
from rest_framework import serializers
class WriterSerializer(serializers.Serializer):
pass
Смотрим, какие в модели есть поля, куда будут записываться пришедшие в запросе данные. В примере класс модели показан в сокращённом варианте.
class Writer(models.Model):
firstname = models.CharField(max_length=100...)
lastname = models.CharField(max_length=100...)
patronymic = models.CharField(max_length=100...)
birth_place = models.ForeignKey(to=Town...)
birth_date = models.DateField(...)
Итак, нам нужно, чтобы в POST-запросе пришли данные для пяти полей.
Подбираем корреспондирующие поля сериалайзера
Каждое поле сериалайзера мы назовём так же, как поле модели, которое оно обслуживает. Это позволит не указывать дополнительный атрибут source
.
Поля firstname, lastname и patronymic
Одноимённые поля модели ожидают обычные строковые значения, поэтому для корреспондирующих полей сериалайзера выберем класс serializers.Charfield
.
Заглянем в метод __init__
этого же класса, чтобы определиться с аргументами при создании поля.
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
self.trim_whitespace = kwargs.pop('trim_whitespace', True)
self.max_length = kwargs.pop('max_length', None)
self.min_length = kwargs.pop('min_length', None)
...
allow_blank
трогать не будем — нам нужны данные, поэтому пусть остаётся дефолтныйFalse
.trim_whitespace
обрежет пробелы перед текстом и после него. Нам это подходит, поэтому атрибут тоже не трогаем и оставляем дефолтныйTrue
.max_length
нам нужен, потому что такой же валидатор стоит у каждого текстового поля моделиWriter
. Поскольку по дефолту он не задан, объявим его явно и приведём лимит по количеству символов — такой же, как и в модели.min_length
нам не потребуется, потому что у нас нет ограничений по минимальному количеству символов для полей модели.
Получаем следующий код:
class WriterSerializer(serializers.Serializer):
firstname = serializers.CharField(max_length=100)
patronymic = serializers.CharField(max_length=100)
lastname = serializers.CharField(max_length=100)
Поле birth_place
Одноимённое поле модели относится к классу ForeignKey
и связано с моделью Town
.
class Writer(models.Model):
...
birth_place = models.ForeignKey(to=Town...)
class Town(models.Model):
name = models.CharField(max_length=100, unique=True, ...)
Тут нам нужно не просто передать какое-то значение (тот же текст), которое сразу запишется в базу: нам нужно, чтобы по этому значению был извлечён объект записи из модели Town
.
DRF предоставляет несколько классов полей для работы с полями отношений. Нам подходит класс SlugRelatedField
. Вот его описание из исходного кода: A read-write field that represents the target of the relationship by a unique 'slug' attribute
. Слово slug
может немного путать: под ним здесь понимается любое уникальное поле модели с любым названием. И совсем необязательно, чтобы оно называлось slug
или относилось к классу SlugField
.
В предыдущей статье мы разобрали, что при работе сериалайзера на запись в поле любого класса работает метод to_internal_value
. Вот его исходный код для класса SlugRelatedField
:
def to_internal_value(self, data):
queryset = self.get_queryset()
try:
return queryset.get(**{self.slug_field: data})
except ObjectDoesNotExist:
self.fail('does_not_exist', slug_name=self.slug_field, value=smart_str(data))
except (TypeError, ValueError):
self.fail('invalid')
Код показывает, какие атрибуты нам следует передать, а именно:
queryset
— набор записей (разумеется, из связанной модели);slug_field
— имя уникального поля в связанной модели, которое будет использоваться для ORM-запроса.get
. Всегда можно указатьpk
илиid
, но у нас есть уникальное полеname
, поэтому выберем его.
Что касается набора записей, мы будем искать объект Town
по всем записям, поэтому передадим Town.objects.all()
. Но можно сократить его до Town.objects
, потому что all()
будет вызван под капотом.
Итог:
class WriterSerializer(serializers.Serializer):
...
birth_place = serializers.SlugRelatedField(slug_field='name', queryset=Town.objects)
Поле birth_date
В django-модели поле birth_date
относится к классу DateField
. Одноимённый класс предусмотрен и среди полей сериалайзера.
При объявлении поля DateField
можно передать два необязательных аргумента:
format
— формат, в котором дата будет возвращаться при работе сериалайзера на чтение;input_formats
— список или кортеж допустимых форматов передачи даты при работе сериалайзера на запись.
Поскольку сейчас мы акцентируемся на работе сериалайзера на запись, рассмотрим подробнее второй аргумент. Если его не передать, то применится настройка из-под капота DATE_INPUT_FORMATS
. Она позволяет передать дату в формате iso-8601
— проще говоря, строкой вида YYYY-MM-DD
.
Чтобы глобально переопределить это поведение, нужно прописать в настройках DRF собственные форматы. Они должны отвечать требованиям Python-модуля datetime
.
Все настройки DRF прописываются в settings.py
django-проекта, а именно в словаре REST_FRAMEWORK
. Перенастроим формат передачи строки с датой:
# settings.py
REST_FRAMEWORK = {
'DATE_INPUT_FORMATS': [
'%d.%m.%Y', # '25.10.2021'
'%d.%m.%y', # '25.10.21'
]
}
Для большей выразительности примера мы переопределим формат ввода/вывода даты не глобально, а прямо в поле сериалайзера.
birth_date = serializers.DateField(
format='%d.%m.%Y', # из базы дата будет вытаскиваться в формате "25.10.2021"
input_formats=['%d.%m.%Y', 'iso-8601',] # в том же виде сериалайзер ожидает дату «на вход», но можно и дефолтный формат
)
Под капотом DateField
переданная строка превратится в объект класса datetime.date
с помощью datetime.datetime.strptime
.
Собираем все поля вместе и проверяем работу сериалайзера
Код нашего сериалайзера получился таким:
class WriterSerializer(serializers.Serializer):
firstname = serializers.CharField(max_length=100)
patronymic = serializers.CharField(max_length=100)
lastname = serializers.CharField(max_length=100)
birth_place = serializers.SlugRelatedField(
slug_field='name',
queryset = Town.objects
)
birth_date = serializers.DateField(
format='%d.%m.%Y',
input_formats=['%d.%m.%Y']
)
Сериалайзер может работать и на запись, и на чтение — read only
или write only
полей нет. Проверим.
Сначала задействуем сериалайзер на чтение. Нам понадобится запись из базы и аргумент instance
(в каких случаях нужен тот или иной аргумент при создании сериалайзера, мы подробно рассматривали в предыдущей статье).
instance = Writer.objects.first()
serializer_for_reading = WriterSerializer(instance=instance)
print(serializer_for_reading.data)
Результат:
{
'firstname': 'Константин',
'patronymic': 'Николаевич',
'lastname': 'Батюшков',
'birth_place': 'Вологда',
'birth_date': '29.05.1787'
}
Теперь посмотрим, как наш сериалайзер работает на запись. Понадобится словарь с входными данными и аргумент data
.
data = {
'firstname': 'Иван',
'patronymic': 'Алексеевич',
'lastname': 'Бунин',
'birth_place': 'Воронеж',
'birth_date': '22.10.1870'
}
serializer_for_writing = WriterSerializer(data=data)
# валидируем входные данные
print(serializer_for_writing.is_valid()) # True
print(serializer_for_writing.errors) # ожидаемо пустой словарь {}
Посмотрим на обработанные сериалайзером данные, готовые к записи в БД, которые находятся в validated_data
:
# serializer_for_writing.validated_data
OrderedDict(
[
('firstname', 'Иван'),
('patronymic', 'Алексеевич'),
('lastname', 'Бунин'),
('birth_place', <Town: Воронеж>),
('birth_date', datetime.date(1870, 10, 22))
]
)
В поле birth_place
находится не строка «Воронеж», а объект модели Town
, готовый для записи в поле с внешним ключом birth_place
. А в поле birth_date
— не строка с датой, а объект класса datetime.date
.
Попробуем передать невалидные данные: например, для поля birth_date
придёт строка в неправильном формате (допустим, 22/10/1870
). В этом случае is_valid
вернёт False
, а словарь errors
будет таким:
{
'birth_date': [
ErrorDetail(string='Неправильный формат date. Используйте один из этих форматов: DD.MM.YYYY.',
code='invalid')
]
}
Все поля сериалайзера отработали штатно. Ура!
Усиливаем валидацию
В предыдущей статье мы говорили о многоступенчатой системе валидации в DRF-сериалайзере. Попрактикуемся в создании валидаторов на разных этапах проверки входных данных.
Валидатор для конкретного поля. Допустим, нас интересуют только писатели, которые родились не позднее XX века. Для проверки этого условия добавим аргумент validators
в поле birthdate
.
from datetime import date
from django.core.validators import MaxValueValidator
...
class WriterSerializer(serializers.Serializer):
...
birth_date = serializers.DateField(..., validators=[MaxValueValidator(date(1999, 12, 31))])
Метавалидатор. Мы хотим, чтобы сочетание «имя – отчество – фамилия» было уникальным. Здесь пригодится UniqueTogetherValidator
, который следует объявить в классе Meta
сериалайзера, а до этого — импортировать из rest_framework.validators
.
class WriterSerializer(serializers.Serializer):
...
class Meta:
validators = [
UniqueTogetherValidator(
queryset=Writer.objects,
fields=['firstname', 'patronymic', 'lastname']
)
]
Заключительная валидация методом validate. Напоследок мы хотим проверить, что имя, фамилия и отчество не повторяются между собой.
Напомню, что в attrs
находится упорядоченный словарь с прошедшими все предыдущие проверки данными.
class WriterSerializer(serializers.Serializer):
...
class Meta:
...
def validate(self, attrs):
set_attrs = set(
[attrs['firstname'], attrs['patronymic'], attrs['lastname']]
)
if len(set_attrs) != 3:
raise ValidationError(
'Имя, отчество и фамилия не могут совпадать между собой',
code='duplicate values'
)
return attrs
Добавляем сериалайзеру возможность записывать валидированные данные в БД
DRF-класс Serializer
наследует от класса BaseSerializer
, у которого есть метод `save`. Но вызвать его напрямую мы пока не можем. Чтобы метод заработал, внутри класса нашего сериалайзера нужно описать два метода:
create
с логикой сохранения в БД новой записи;update
с логикой обновления в БД существующей записи.
Примеры этих методов есть в документации.
Сейчас нам достаточно записывать в БД лишь новые данные, поэтому определим только метод create
:
class WriterSerializer(serializers.Serializer):
# тут следуют определения полей сериалайзера, класс Meta, метод validate — пропускаем их для краткости
def create(self, validated_data):
return Writer.objects.create(**validated_data)
Теперь при вызове у экземпляра сериалайзера метода .save()
без аргументов он вернёт новую запись. Вызывать метод save
можно только после получения валидированных данных — другими словами, после вызова is_valid
.
Разработчики DRF отмечают: логика save
абсолютно не исчерпывается созданием или обновлением записи в БД. Можно вообще не описывать методы create
и update
, а целиком переопределить сам save
. Например, чтобы при его вызове валидированные данные отправлялись по электронной почте.
Контроллер и Browsable API
В первой статье, где демонстрировался пример работы DRF на чтение, мы использовали самый простой контроллер на основе класса APIView
. Задействуем его и в этот раз — понадобится лишь дописать логику метода post
.
Для работы с POST
-запросами в Browsable API
есть удобная вкладка HTML form
, которая предоставляет отдельное поле в форме для каждого поля сериалайзера. Чтобы эта вкладка отрендерилась в шаблоне, в контроллере должен присутствовать атрибут get_serializer
или serializer_class
. Для высокоуровневых контроллеров, начиная с GenericAPIView
, эти атрибуты есть под капотом.
Поскольку мы используем «голый» APIView
, то допишем необходимый атрибут самостоятельно.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Writer
from .serializers import WriterSerializer
class WriterInfoView(APIView):
serializer_class = WriterSerializer
model = Writer
def get(self, request):
... # полный код не приводим — он аналогичен коду из первой статьи о работе API на чтение
def post(self, request):
serializer_for_writing = self.serializer_class(data=request.data)
serializer_for_writing.is_valid(raise_exception=True)
serializer_for_writing.save()
return Response(data=serializer_for_writing.data, status=status.HTTP_201_CREATED)
Посмотрим на представление нашего API в браузере:
Сверху видим ответ на GET
-запрос: в базе пока нет записи ни об одном писателе. Ниже есть удобная форма для отправки POST
-запроса. Заполним и отправим её. Результат:
Итак, мы разобрались, как создать API для валидации входных данных с последующей их записью в базу данных. В следующей статье мы поднимемся на один уровень абстракции вверх и рассмотрим, как устроен класс ModelSerializer
.