Предустановленный класс для работы с моделями: Viewset
На прошлом уроке вы реализовали все типовые операции CRUD, для этого потребовалось лишь два класса-дженерика и два эндпоинта. Шесть операций упакованы в два класса — неплохо, но можно лучше.
Всякий раз, когда снова потребуется реализовать CRUD, придётся снова и снова описывать тот же набор из аналогичных классов. А почему бы не описать всё в одном классе?
И когда у кого-то возник этот вопрос — появились вьюсеты.Вьюсет (англ. viewset, «набор представлений») — это высокоуровневый view-класс, реализующий все операции CRUD; он может вернуть объект или список объектов, создать, изменить или удалить объекты.
Во вьюсеты встроена обработка разных типов запросов, работа с сериализаторами и моделями, фильтрация и пагинация результатов, возврат ошибок. Не нужно ничего придумывать: всё работает «из коробки».
В библиотеке rest_framework есть несколько разных вьюсетов, они хранятся в пакете viewsets.
Начнём с самого популярного вьюсета. Встречайте: ModelViewSet.
Универсальный ModelViewSet
Класс ModelViewSet может выполнять любые операции CRUD с моделью. От разработчика не требуется описывать методы для чтения и записи данных для модели: эти операции уже реализованы.В классе, наследующемся от ModelViewSet, обязательно должны быть описаны два поля:
- в поле
querysetзадаётся выборка объектов модели, с которой будет работать вьюсет; - в поле
serializer_classуказывается, какой сериализатор будет применён для валидации и сериализации.
И во второй раз перепишем Kittygram: вместо дженериков применим вьюсеты.Это практическая работа: вносите изменения, описанные в этом уроке, в проект Kittygram, развёрнутый на вашем компьютере.В начале работы нужно импортировать пакет viewsets и создать класс, наследующийся от ModelViewSet.
# views.py
from rest_framework import viewsets
from .models import Cat
from .serializers import CatSerializer
class CatViewSet(viewsets.ModelViewSet):
queryset = Cat.objects.all()
serializer_class = CatSerializer
Всё. Класс, который обработает все шесть типичных действий, готов.
Класс ModelViewSet предоставляет отличный набор инструментов для всех востребованных операций при работе с моделями. В большинстве стандартных ситуаций он будет работать «из коробки».
Выгода очевидна.
Класс ReadOnlyModelViewSet: только чтение
В пакете rest_framework.viewsets есть похожий на ModelViewSet, но ограниченный в правах класс ReadOnlyModelViewSet. Он может только получать данные модели, а записывать и изменять — не может.
Этот класс полезен в ситуациях, когда требуется только выдавать данные по запросу, без возможности их изменить. В остальном ReadOnlyModelViewSet работает точно так же, как и ModelViewSet.
Роутеры
При работе с view-классами и дженериками каждый эндпоинт отдельно описывается в urls.py. Но для вьюсетов есть более удобный и экономичный инструмент — роутеры (англ. routers).
С помощью роутера для заданных вьюсетов создаются эндпоинты по маске адреса:
URL-префикс/иURL-префикс/<int:pk>.
В DRF есть два стандартных роутера: SimpleRouter и DefaultRouter. Они очень похожи, начнём с первого.Добавьте роутеры в Kittygram. В файл urls.py импортируйте класс SimpleRouter и создайте экземпляр этого класса.
# urls.py
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
Чтобы роутер создал необходимый набор эндпоинтов, необходимо вызвать его метод register() (говорят «зарегистрировать эндпоинты»). В качестве аргументов этот метод принимает URL-префикс и название вьюсета, для которого создаётся набор эндпоинтов.
router.register('cats', CatViewSet)
После регистрации надо включить новые эндпоинты в список urlpatterns: перечень эндпоинтов будет доступен в router.urls.
Создание эндпоинтов через router выглядит так:
# urls.py
from rest_framework.routers import SimpleRouter
from django.urls import include, path
from cats.views import CatViewSet
# Создаётся роутер
router = SimpleRouter()
# Вызываем метод .register с нужными параметрами
router.register('cats', CatViewSet)
# В роутере можно зарегистрировать любое количество пар "URL, viewset":
# например
# router.register('owners', OwnerViewSet)
# Но нам это пока не нужно
urlpatterns = [
# Все зарегистрированные в router пути доступны в router.urls
# Включим их в головной urls.py
path('', include(router.urls)),
]
Только что созданный роутер сгенерирует два эндпоинта:
cats/,cats/<int:pk>/.
Теперь через эти эндпоинты будут доступны любые операции с моделью:
- POST-запрос на
cats/создаст новую запись. - Запросы PUT, PATCH или DELETE к адресу
cats/<pk>/изменят или удалят существующую запись. - GET-запрос на те же адреса вернёт список объектов или один объект.
Параметр name в эндпоинтах
В urls.py для каждого маршрута можно указать необязательный параметр name. Если этот параметр определён, то во view-функциях или view-классах через функцию reverse() можно получить соответствующие URL — это очень удобно и соответствует принципу DRY.
На практике вы уже работали с этими функциями при написании тестов для Django-проекта; при тестировании API name в эндпоинтах вам тоже понадобятся.
Роутеры сами автоматически создают name для каждого эндпоинта, его значение создаётся
- из имени модели, с которой работает вьюсет,
- и суффикса:
- -list (для эндпоинта, работающего с коллекцией объектов)
- или -detail (для эндпоинта, работающего отдельным объектом).
Например, для роутера, созданного для вьюсета CatViewSet().
class CatViewSet(viewsets.ModelViewSet):
queryset = Cat.objects.all()
serializer_class = CatSerializer
имена эндпоинтов будут такими:
urlpatterns = [
# Здесь имя "cat" взято из queryset,
# с которым работает вьюсет CatViewSet
path('cat/', ..., name='cat-list'),
path('cat/<int:pk>/', ..., name='cat-detail'),
]
При работе с вьюсетами и роутерами вы можете столкнуться с ошибкой «не определён аргумент basename»:
'basename' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.queryset' attribute.
Речь идёт о необязательном аргументе роутера basename: в нём можно вручную указать префикс для параметра name в эндпоинтах, созданных роутером.Например, можно переопределить префикс cat в name='cat-list' и name='cat-detail'
router.register('cats', CatViewSet, basename='tiger')
В результате name для эндпоинтов будут начинаться с tiger:
urlpatterns = [
path('cat/', ..., name='tiger-list'),
path('cat/<int:pk>/', ..., name='tiger-detail'),
]
Однако есть случаи, когда параметр basename обязательно должен быть указан. Это необходимо в тех случаях, когда queryset однозначно не задан во вьюсете, а определён через метод get_queryset().
# Если бы пользователи могли оставлять комментарии к котикам,
# то эндпоинт для работы с комментариями выглядел бы примерно так:
# cats/{cat_id}/comments/
class CommentViewSet(viewsets.ModelViewSet):
serializer_class = CommentSerializer
# queryset во вьюсете не указываем
# Нам тут нужны не все комментарии, а только связанные с котом с id=cat_id
# Поэтому нужно переопределить метод get_queryset и применить фильтр
def get_queryset(self):
# Получаем id котика из эндпоинта
cat_id = self.kwargs.get("cat_id")
# И отбираем только нужные комментарии
new_queryset = Comment.objects.filter(cat=cat_id)
return new_queryset
В подобных ситуациях создать name автоматически не получится, и параметр basename придётся указать явным образом.
SimpleRouter vs DefaultRouter
DefaultRouter — это расширенная версия SimpleRouter: он умеет всё то же, что и SimpleRouter, а в дополнение ко всему генерирует корневой эндпоинт /, GET-запрос к которому вернёт список ссылок на все ресурсы, доcтупные в API.У этого эндпоинта тоже есть name: api-root. Маршрут для него мог бы выглядеть примерно так:
urlpatterns = [
...
path('', ..., name='api-root'),
]
Если применён DefaultRouter, то в ответ на GET-запрос к адресу http://127.0.0.1:8000/ вернётся список ссылок на доступные ресурсы.
При работе с документированием вам потребуется получить коллекцию ссылок на все ресурсы API, так что далее в курсе будем работать именно с DefaultRouter.
