Viewsets и ModelViewSet

Курс по Django: https://stepik.org/a/183363

Давайте посмотрим на код, который у нас с вами получился в файле women/views.py. Здесь налицо его дублирование в классах представлениях:

class WomenAPIList(generics.ListCreateAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
 
 
class WomenAPIUpdate(generics.UpdateAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
 
 
class WomenAPIDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer

Конечно, если проект небольшой, то это не особо критично и можно было бы оставить в таком виде. Но при масштабировании этой программы, при добавлении новых моделей и API-представлений, связанных с ними, у нас это дублирование будет очень быстро увеличиваться и программу будет уже не так удобно поддерживать и менять. Благо разработчики Django REST Framework этим озаботились и предоставили нам инструмент устранения такой избыточности. Это достигается за счет использования вьюсетов (viewsets) – классов, которые объединяют функциональность нескольких разных вьюх (views). И сейчас мы разберемся, как их применять на практике.

Вначале я сделаю очень простую вещь, классы WomenAPIList, WomenAPIUpdate и WomenAPIDetailView поставлю в комментарии, а вместо них запишу один общий класс представления, заменяющий эти три:

from rest_framework import viewsets
 
 
class WomenViewSet(viewsets.ModelViewSet):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer

Смотрите, вначале, конечно же, нужно импортировать ветку viewsets для более удобного доступа к вьюсетам, а затем, определяем новый дочерний класс с именем WomenViewSet, наследуемый от базового ModelViewSet. Почему именно ModelViewSet? Ну, я думаю, вы догадались? Так как наши представления работают с моделью Women, то и вьюсет должен поддерживать работу с моделями. Поэтому я им и воспользовался. Вообще, полный список существующих классов-вьюсетов в DRF можно посмотреть на следующей странице официальной документации:

https://www.django-rest-framework.org/api-guide/viewsets/

Как видите, их не так много:

ViewSet, GenericViewSet, ModelViewSet, ReadOnlyModelViewSet

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

Хорошо, мы определили наш собственный вьюсет WomenViewSet. Но, что он может делать? Какой функционал в нем скрыт? Я покажу это, следующим образом. Давайте перейдем в файл drfsite/urls.py и пропишем маршруты для нашего вьюсета.

Вначале сделаем это привычным нам способом – через метод as_view(). В этом методе для классов вьюсетов можно указывать словарь, где ключ – это тип запроса от клиента (GET, POST, PUT, DELETE и т.п.), а значение – метод, который должен при этом запросе вызываться:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/womenlist/', WomenViewSet.as_view({'get': 'list'})),
    path('api/v1/womenlist/<int:pk>/', WomenViewSet.as_view({'put': 'update'})),
]

В частности, метод as_view({'get': 'list'}) связывает GET-запрос с возвратом клиенту списка записей из модели таблицы БД. А метод as_view({'put': 'update'}) связывает PUT-запрос с методом update() для изменения текущей записи. Но откуда я знаю, какие методы можно писать в этом словаре? Все написано в документации. Здесь в разделе «ViewSet actions» перечислены все стандартные методы вьюсетов. Что они делают, легко понять по их названиям. Соответственно, каждый из типов запроса можно связать с одним из этих методов.

Давайте посмотрим, как это будет работать. Запускаем тестовый веб-сервер, открываем страницу в браузере:

http://127.0.0.1:8000/api/v1/womenlist/

и видим список записей из таблицы women. Если же дополнительно прописать идентификатор статьи:

http://127.0.0.1:8000/api/v1/womenlist/9/

то получим способ ее изменения методом PUT.

Роутеры (routers) для вьюсетов (viewsets)

Однако, это не лучший способ описания маршрутов для класса вьюсета. Мы видим, что здесь один и тот же класс связан с несколькими маршрутами, причем, формат этих URL-путей имеет определенную закономерность: если указывать путь без идентификатора, то получаем список записей, а с идентификатором – работаем с одной конкретной записью. И это довольно частый случай. Поэтому и он был автоматизирован на уровне Django REST Framework с использованием, так называемых, роутеров. Подробнее о них можно почитать на странице официальной документации:

https://www.django-rest-framework.org/api-guide/routers/

Понять работу роутера проще всего на конкретном примере. Давайте в нашем проекте создадим роутер для класса WomenViewSet. Вначале мы импортируем ветку routers:

from rest_framework import routers

а, затем, создадим объект простого роутера (SimpleRouter):

router = routers.SimpleRouter()

Следующим шагом нам нужно связать этот роутер с вьюсетом WomenViewSet. Делается это с помощью метода register:

router.register(r'women', WomenViewSet)

Здесь первым аргументом указана raw-строка «women» - это префикс (prefix) для URL-адреса, по которому будет доступен данный вьюсет (далее мы увидим, где этот префикс фигурирует). А вторым аргументом – сам класс WomenViewSet.

Все, осталось только связать маршруты роутера с самим проектом (сайтом). Для этого я воспользуюсь уже знакомой нам функцией include (по урокам Django) и добавлю, автоматически сгенерированные пути, в коллекцию urlpatterns:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(router.urls)),   # http://127.0.0.1:8000/api/v1/women/
]

Смотрите, роутер SimpleRouter формирует два типа маршрутов:

  • http://127.0.0.1:8000/api/v1/women/ - для извлечения списка записей;
  • http://127.0.0.1:8000/api/v1/women/pk/ - для работы с конкретной записью.

Давайте посмотрим, что у нас получится. Переходим в веб-браузер и по первому URL-пути получаем список вместе с формой добавления новой записи. Такой функционал по умолчанию предоставляет базовый класс ModelViewSet. Если же укажем второй тип маршрута (с идентификатором записи), то отобразится запись (если она была найдена) и будет возможность ее изменить и удалить. То есть, мы здесь получили полный функционал по работе с записями таблицы БД с использованием одного класса вьюсета.

Если же мы изменим базовый класс на ReadOnlyModelViewSet:

class WomenViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer

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

Тонкая настройка функциональности класса-вьюсета

Давайте посмотрим, как реализованы эти базовые классы. Фактически, они просто наследуют определенный набор классов-миксинов плюс базовый класс GenericViewSet:

class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
    pass
 
 
class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
   pass

Это значит, что мы можем легко определить свой вьюсет с нужным набором функционала. Например, следующим образом:

class WomenViewSet(mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   viewsets.GenericViewSet):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer

В результате, получим полный функционал кроме добавления новой записи. Или, если убрать mixins.DestroyModelMixin, то не сможем удалять конкретную запись. И так далее. То есть, меняя набор классов-миксинов, мы автоматически определяем нужный нам функционал класса-вьюсета. И это очень удобно.

Курс по Django: https://stepik.org/a/183363