View-функции API

Запрос к API проходит такой путь:

  • urls.py — здесь настраивается маршрутизация API, описываются эндпоинты для ресурсов.
  • serializers.py — здесь данные преобразуются из JSON в объекты Python и обратно;
  • views.py — здесь идёт обработка параметров запроса, получение или запись данных, создание ответа.

Для обработки запросов к API применяются view-функции или view-классы. Функции, как правило, позволяют лучше понять, что происходит в коде; вот с них и начнём.

View-функции API

Чтобы создать view-функцию для API, нужно обеспечить её работу со специальными объектами Request и Response, которые, по сути, являются модификациями объектов HttpRequest и HttpResponse, используемых в Django.В DRF у объекта request появляется атрибут data, он чем-то похож на уже знакомый вам request.POST. Отличия заключаются в том, что request.

POST содержит только данные из формы и работает только с методом POST. А request.data содержит словарь со всеми данными из запроса и работает с методами POST, PUT и PATCH.

View-функция API должна возвращать объект Response, которому передаются сериализованные данные. В нашем случае «сериализованные данные» — это JSON: ведь мы работаем с архитектурой REST.

Декоратор @api_view

Для «перенастройки» view-функции на работу с API в Django REST framework есть стандартный декоратор @api_view.Среди возможностей, которые он предоставляет, выделим следующие:

  • @api_view определяет разрешённые методы запросов;
  • @api_view позволяет оперировать модифицированными объектами Request и Response.

Рассмотрим простую view-функцию hello() в Django: при GET-запросе она вернёт ответ в текстовом формате: «Это был GET-запрос!», а при POST-запросе — вернёт строку 'Получены данные: <словарь>' со словарём request.POST.

from django.http import HttpResponse

def hello(request):
    if request.method == 'POST':
        return HttpResponse(f'Получены данные: {request.POST}')
    elif request.method == 'GET':
        return HttpResponse('Это был GET-запрос!') 

Чтобы превратить hello() во view-функцию для API, нужно декорировать её и немного изменить ответы. В параметрах декоратора @api_view указываются типы запросов, которые разрешены для декорируемой view-функции: @api_view([<разрешённые HTTP-методы>]).

from rest_framework.decorators import api_view  # Импортировали декоратор
from rest_framework.response import Response


@api_view(['GET', 'POST'])  # Добавили декоратор и указали разрешённые методы
def hello(request):
    if request.method == 'POST':
        # Изменили тип возвращаемого объекта,
                #теперь это Response и вложенный JSON
        return Response({'message': 'Получены данные', 'data': request.data})
    # Изменили ответ
    return Response({'message': 'Это был GET-запрос!'}) 

В декораторе @api_view указаны два разрешённых метода — GET и POST. В ответ на запросы других типов view-функция вернёт ответ “Error 405 Method Not Allowed” — это предусмотрено в декораторе @api_view.

  • Данные из POST-запроса передаются в декорированную view-функцию hello() в словаре request.data. В ответ на POST-запрос view-функция hello() вернёт JSON с данными, которые были получены в запросе.
  • В ответ на GET-запрос view-функция вернёт заранее заготовленную фразу:
# Ответ на GET-запрос
{
    "message": "Это был GET-запрос!"
} 

Для наглядности в ответ добавлены ключи 'message' и 'data', имена ключей в этом примере ни на что не влияют.Готово: простенькая view-функция API готова обрабатывать запросы!

View-функция API: валидация и сериализация отдельного объекта и списка объектов

View-функция hello() достаточно проста — она просто возвращает полученные данные, не обрабатывая и даже не проверяя их: что передали — на том и спасибо.

В реальном мире view-функция должна быть сложнее; на примере cat_list() из Kittygram разберёмся, как работает этот механизм.В модели Cat хранится имя котика name, его цвет color и год рождения birth_year.

Начнём с создания новой записи в БД: передадим в POST-запросе новый объект на эндпоинт cats/. При получении запроса данные из него сохраняются в словарь request.data:

# request.data
{
    "name": "Стёпа",
    "color": "белый",
    "bitrh_year": 1971
} 

Прежде чем сохранить эти данные в базе, нужно их валидировать: удостовериться, что они соответствуют ожиданиям модели Cat. Данные из запроса передаются сериализатору, связанному с этой моделью: он провалидирует их и, если валидация пройдёт успешно, десериализует.Сериализатор уже готов: это CatSerializer.

# kittygram/cats/serializers.py
from rest_framework import serializers

from .models import Cat


class CatSerializer(serializers.ModelSerializer):
    class Meta:
        model = Cat
        # В прошлом уроке fields = '__all__' изменили:
        fields = ('name', 'color', 'birth_year') 

При создании экземпляра сериализатора в конструктор класса передаём словарь request.data, в котором содержатся данные из запроса. Именно их нужно валидировать и сериализовать.Данные передаются в сериализатор через именованный параметр data.

# View-функция
@api_view(['GET', 'POST'])
def cat_list(request):
    if request.method == 'POST':  # Сейчас нас интересует только POST-запрос.
        serializer = CatSerializer(data=request.data)
# В ответ на GET-запрос сейчас вернётся ошибка 500 Internal Server Error 

Валидация входящих данных

Для валидации полученных данных надо вызвать в сериализаторе метод is_valid(). В зависимости от результатов валидации можно среагировать на запрос по-разному:

  • если валидация прошла успешно — сохраним запись в БД при помощи метода save() и вернём созданный объект;
  • если валидация не пройдена — вернём объект serializer.errors, в нём будет сохранён словарь с возникшими при валидации ошибками.
@api_view(['GET', 'POST'])
def cat_list(request):
    if request.method == 'POST':
        serializer = CatSerializer(data=request.data)
        if serializer.is_valid():
              # Если полученные данные валидны — выполняем работу
                # Например, сохраняем данные в базу через save() (как при работе с формами)
            serializer.save()
                # Возвращаем JSON с полученными данными и статус-код 200
                return Response(serializer.data, status=status.HTTP_200_OK)
        # Если данные не прошли валидацию — возвращаем сообщение об ошибке:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 

Обработка POST-запроса со списком объектов

Следующая задача: научить view-функцию API принимать и обрабатывать список объектов. Такой подход будет востребован: если владелец котофермы захочет добавить в базу всех своих котов, ему не придётся отправлять сотню отдельных запросов.

Для этого к API нужно отправить POST-запрос с несколькими объектами Cat. В этом случае в словарь request.data будет передан не словарь из полей name,colorи birth_year, а список объектов, у каждого из которых будут собственные поля с такими названиями.

# request.data со списком объектов

[
    {
        "name": "Стёпа",
        "color": "белый",
        "birth_year": 1971
    },
    {
        "name": "Мурка",
        "color": "рыжий",
        "birth_year": 2010
    },
    {
        "name": "Пушок",
        "color": "чёрный",
        "birth_year": 2018
    }
] 

Чтобы сериализатор был готов принять список объектов, в конструктор сериализатора нужно дополнительно передать именованный параметр many=True.

serializer = CatSerializer(data=request.data, many=True) 

В результате обработка запроса пройдёт по тому же принципу, что и с одним объектом.

Если этот параметр не указан, сериализатор не станет обрабатывать список объектов и вернёт ошибку: "Invalid data. Expected a dictionary, but got list."

Обработка GET-запроса на получение списка объектов

Вернуть список объектов в ответ на GET-запрос тоже не проблема. При подготовке ответа view-функция должна получить из базы данных queryset, в котором будут храниться запрошенные объекты модели (например, все объекты модели Cat). Для сериализации ответа этот queryset передаётся в конструктор сериализатора первым параметром, а вторым указывается many=True:

# Получаем все объекты модели
cats = Cat.objects.all()
# Передаём queryset в конструктор сериализатора
serializer = CatSerializer(cats, many=True) 

В результате view-функция API cat_list() примет такой вид:

@api_view(['GET', 'POST'])  # Разрешены только POST- и GET-запросы
def cat_list(request):
    # В случае POST-запроса добавим одну запись или список записей в БД
    if request.method == 'POST':
        serializer = CatSerializer(data=request.data, many=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    # В случае GET-запроса возвращаем список всех котиков
    cats = Cat.objects.all()
    serializer = CatSerializer(cats, many=True)
    return Response(serializer.data) 

Если в сериализаторе не указан параметр many=True, при обработке GET-запроса вернётся ошибка AttributeError.

Создать vs обновить

Результат вызова метода save() может быть разным: может быть создана новая запись в БД, а может быть обновлена существующая запись.

Для создания новой записи в сериализатор передаются только данные из запроса; объект будет создан при вызове метода save().

serializer = CatSerializer(data=request.data)
# Если вызвать serializer.save(), будет создан новый экземпляр 

Для обновления существующей записи первым параметром в сериализатор передаётся тот объект модели, который нужно обновить. В этом случае вызов save() не приведёт к созданию нового объекта.

cat = cat.objects.get(id=id)
serializer = CatSerializer(cat, data=request.data)
# Если вызвать serializer.save(), будет обновлён существующий экземпляр cat 

Сериализатор и метод PATCH

По умолчанию сериализатору должны быть переданы значения всех полей, перечисленных в его параметре fields. Однако в PATCH-запросе могут быть переданы не все поля, а лишь те, которые нужно изменить. По умолчанию в такой ситуации сериализатор вызовет ошибку 400 Bad Request "This field is required.": в запросе не передано обязательное поле!

Решить эту проблему поможет аргумент partial=True: если при создании сериализатора указать этот аргумент — отсутствие в запросе обязательных полей не вызовет ошибку.

serializer = CatSerializer(cat, data=request.data, partial=True) 

Разделение обязанностей между view-функциями API

Для API программой-минимум должна быть реализация шести операций для нужной модели:

  • создание нового объекта;
  • получение информации об объекте;
  • удаление объекта;
  • замещение объекта (целиком);
  • изменение одного или нескольких полей объекта;
  • получение списка объектов.

Хорошей практикой считается сгруппировать эти операции в две view-функции.

В одной view-функции реализуется создание нового объекта и запрос всех объектов коллекции, например:

  • POST-запрос на адрес cats/ создаст новую запись о котике;
  • GET-запрос на тот же адрес вернёт список всех котиков.

Вторая view-функция обрабатывает запросы для получения, изменения и удаления одиночного объекта:

  • GET-запрос к адресу cats/<pk>/ вернёт один объект по его id;
  • запросы PUT, PATCH или DELETE к тому же адресу перезапишут, изменят или удалят существующую запись о котике.

В результате в urls.py потребуется описать лишь два эндпоинта:

  • cats/,
  • cats/<int:pk>/.
    А ссылаться эти эндпоинты будут на две view-функции во views.py, например:
  • cat_list(),
  • cat_detail().

Время кодить

Пришло время самостоятельно реализовать CRUD на Django REST framework с помощью view-функций API.

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

  • View-функция обрабатывает только те запросы, которые указаны в декораторе @api_view([<разрешённые HTTP-методы>]).
  • Данные из POST-запроса сохраняются в словаре request.data и передаются в конструктор сериализатора через именованный параметр datadata=request.data.
  • Вместе с ответом необходимо вернуть клиенту правильный статус-код. Список кодов можно уточнить здесь.




Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: