Предустановленный класс для работы с моделями: 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.