Django REST Framework Views - ViewSets

До этого момента мы рассматривали создание отдельных представлений с помощью APIViews и Общих представлений. Часто имеет смысл объединить логику представления для набора связанных представлений в один класс. Это может быть достигнуто в Django REST Framework (DRF) путем расширения одного из классов ViewSet.

Классы ViewSet устраняют необходимость в дополнительных строках кода и в сочетании с маршрутизаторами помогают поддерживать согласованность ваших URL-адресов.

Серия просмотров фреймворка Django REST:

  1. Просмотры приложений
  2. Общие представления
  3. Наборы просмотров (эта статья!)

Наборы просмотров

ViewSet - это тип представления на основе классов.

Вместо обработчиков методов, таких как .get() и .post(), он предоставляет действия, такие как .list() и .create().

Наиболее значительным преимуществом ViewSets является то, что построение URL-адресов выполняется автоматически (с помощью класса router). Это помогает обеспечить согласованность соглашений об URL-адресах в вашем API и минимизирует объем кода, который вам нужно написать.

Существует четыре типа наборов представлений, от самых простых до самых мощных:

  1. Набор представлений
  2. Общий набор представлений
  3. Только для чтения ModelViewSet
  4. ModelViewSet

В основном они построены на основе классов, с которыми вы познакомились в предыдущей статье из этой серии:

DRF ViewSets Overview

ViewSetMixin это класс, в котором происходит все "волшебство". Это единственный класс, который используют все четыре набора представлений. Он переопределяет метод as_view и объединяет этот метод с соответствующим действием.

Method List / Detail Action
post List create
get List list
get Detail retrieve
put Detail update
patch Detail partial_update
delete Detail destroy

Класс ViewSet

Класс ViewSet использует преимущества класса APIView. По умолчанию он не предоставляет никаких действий, но вы можете использовать его для создания собственного набора представлений:

from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet

class ItemViewSet(ViewSet):
    queryset = Item.objects.all()

    def list(self, request):
        serializer = ItemSerializer(self.queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        item = get_object_or_404(self.queryset, pk=pk)
        serializer = ItemSerializer(item)
        return Response(serializer.data)

Этот набор представлений предоставляет HTTP-метод GET, сопоставленный с действием list (для перечисления всех экземпляров) и действием retrieve (для получения единственного экземпляра).

Действия

Следующие действия выполняются классом маршрутизатора по умолчанию:

  1. list
  2. create
  3. retrieve ( требуется пк)
  4. update ( требуется пк)
  5. partial_update ( требуется пк)
  6. destroy ( требуется пк)

Вы также можете создавать пользовательские действия с помощью @action декоратора.

Например:

from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet

class ItemsViewSet(ViewSet):

    queryset = Item.objects.all()

    def list(self, request):
        serializer = ItemSerializer(self.queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        item = get_object_or_404(self.queryset, pk=pk)
        serializer = ItemSerializer(item)
        return Response(serializer.data)

    @action(detail=False, methods=['get'])
    def items_not_done(self, request):
        user_count = Item.objects.filter(done=False).count()

        return Response(user_count)

Здесь мы определили пользовательское действие, называемое items_not_done.

Разрешенным HTTP-методом является GET.

Здесь мы задали это явно, но функция GET разрешена по умолчанию.

Параметр methods является необязательным, в то время как параметр detail таковым не является. Параметр detail должен быть установлен как True, если действие предназначено для одного объекта, или False, если оно предназначено для всех объектов.

Это действие доступно по умолчанию по следующему URL-адресу: /items_not_done. Чтобы изменить этот URL-адрес, вы можете задать параметр url_path в декораторе.

Если вы уже давно пользуетесь ViewSets, то, возможно, помните декораторы @list_route и @detail_route вместо @action. Они устарели начиная с версии 3.9.

Обработка URL-адресов

Хотя вы могли бы сопоставить URL-адреса ваших наборов представлений так же, как и с другими представлениями, это не относится к наборам представлений.

Вместо использования urlpatterns от Django Наборы представлений поставляются с классом router, который автоматически генерирует конфигурации URL.

DRF поставляется с двумя готовыми маршрутизаторами:

  1. Простой маршрут
  2. Маршрут по умолчанию

Основное различие между ними заключается в том, что маршрутизатор по умолчанию включает в себя корневое представление API по умолчанию:

DRF Browsable API

В корневом представлении API по умолчанию отображаются списки с гиперссылками, что упрощает навигацию по приложению.

Также возможно создать пользовательский маршрутизатор.

Маршрутизаторы также можно комбинировать с url-шаблонами:

# urls.py

from django.urls import path, include
from rest_framework import routers

from .views import ChangeUserInfo, ItemsViewSet

router = routers.DefaultRouter()
router.register(r'custom-viewset', ItemsViewSet)

urlpatterns = [
    path('change-user-info', ChangeUserInfo.as_view()),
    path('', include(router.urls)),
]

Здесь мы создали маршрутизатор (используя маршрутизатор по умолчанию, поэтому мы получаем представление API по умолчанию) и зарегистрировали в нем ItemsViewSet. При создании маршрутизатора вы должны указать два аргумента:

  1. Префикс URL-адреса для представлений
  2. Представление само установилось

Затем мы включили маршрутизатор внутрь urlpatterns.

Это не единственный способ включить маршрутизаторы. Дополнительные опции приведены в документации по Маршрутизаторам.

Во время разработки список элементов будет доступен по адресу http://127.0.0.1:8000/custom-viewset/, а отдельный элемент будет доступен по адресу http://127.0.0.1:8000/custom-viewset/{id}/.

Поскольку мы определили только действия list и retrieve в нашем ItemsViewSet, единственным разрешенным методом является GET.

Наше пользовательское действие будет доступно по адресу http://127.0.0.1:8000/custom-viewset/items_not_done/.

Вот как маршрутизатор сопоставляет методы с действиями:

# https://github.com/encode/django-rest-framework/blob/3.12.4/rest_framework/routers.py#L83

routes = [
        # List route.
        Route(
            url=r'^{prefix}{trailing_slash}$',
            mapping={
                'get': 'list',
                'post': 'create'
            },
            name='{basename}-list',
            detail=False,
            initkwargs={'suffix': 'List'}
        ),
        # Dynamically generated list routes. Generated using
        # @action(detail=False) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=False,
            initkwargs={}
        ),
        # Detail route.
        Route(
            url=r'^{prefix}/{lookup}{trailing_slash}$',
            mapping={
                'get': 'retrieve',
                'put': 'update',
                'patch': 'partial_update',
                'delete': 'destroy'
            },
            name='{basename}-detail',
            detail=True,
            initkwargs={'suffix': 'Instance'}
        ),
        # Dynamically generated detail routes. Generated using
        # @action(detail=True) decorator on methods of the viewset.
        DynamicRoute(
            url=r'^{prefix}/{lookup}/{url_path}{trailing_slash}$',
            name='{basename}-{url_name}',
            detail=True,
            initkwargs={}
        ),
    ]

GenericViewSet

В то время как ViewSet расширяется APIView, GenericViewSet расширяется GenericAPIView.

Класс GenericViewSet предоставляет базовый набор универсальных методов просмотра вместе с методами get_object и get_queryset.

Вот как создаются классы ViewSet и GenericViewSet:

# https://github.com/encode/django-rest-framework/blob/3.12.4/rest_framework/viewsets.py#L210
class ViewSet(ViewSetMixin, views.APIView):
   pass


# https://github.com/encode/django-rest-framework/blob/3.12.4/rest_framework/viewsets.py#L217
class GenericViewSet(ViewSetMixin, generics.GenericAPIView):
   pass

Как вы можете видеть, они оба расширяют ViewSetMixin и либо APIView, либо GenericAPIView. Кроме этого, дополнительного кода нет.

Чтобы использовать класс GenericViewSet, вам нужно переопределить класс и либо использовать смешанные классы, либо явно определить реализации действий для достижения желаемого результата.

Использование GenericViewSet с миксинами

from rest_framework import mixins, viewsets

class ItemViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):

    serializer_class = ItemSerializer
    queryset = Item.objects.all()

Этот GenericViewSet комбинируется со смешиванием ListModelMixin и RetrieveModelMixin. Поскольку это набор представлений, маршрутизатор выполняет сопоставление URL-адресов, а микшины предоставляют действия для просмотра списка и подробных сведений.

Использование GenericViewSet с явными реализациями действий

При использовании миксинов вам нужно только указать атрибуты serializer_class и queryset; в противном случае вам нужно будет выполнить действия самостоятельно.

Чтобы подчеркнуть преимущество GenericViewSet перед ViewSet, мы приведем несколько более сложный пример:

from rest_framework import status
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

class ItemViewSet(GenericViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all()
    permission_classes = [DjangoObjectPermissions]

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save(serializer)

        return Response(serializer.data, status=status.HTTP_201_CREATED)

    def list(self, request):
        serializer = self.get_serializer(self.get_queryset(), many=True)
        return self.get_paginated_response(self.paginate_queryset(serializer.data))

    def retrieve(self, request, pk):
        item = self.get_object()
        serializer = self.get_serializer(item)
        return Response(serializer.data)

    def destroy(self, request):
        item = self.get_object()
        item.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Здесь мы создали набор представлений, который позволяет выполнять действия create, list, retrieve, и destroy.

Поскольку мы расширили GenericViewSet, мы:

  1. Использовал DjangoObjectPermissions и нам не нужно было самостоятельно проверять права доступа к объекту
  2. Вернул ответ с разбивкой по страницам

ModelViewSet

ModelViewSet предоставляет действия по умолчанию create, retrieve, update, partial_update, destroy и list, поскольку он использует GenericViewSet и все доступные миксины.

ModelViewSet это самый простой в использовании вид. Вам нужно всего три строки:

class ItemModelViewSet(ModelViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all()

Затем, после того как вы зарегистрируете просмотр на маршрутизаторе, все готово!

# urls.py

from django.urls import path, include
from rest_framework import routers

from .views import ChangeUserInfo, ItemsViewSet, ItemModelViewSet

router = routers.DefaultRouter()
router.register(r'custom-viewset', ItemsViewSet)
router.register(r'model-viewset', ItemModelViewSet) # newly registered ViewSet

urlpatterns = [
    path('change-user-info', ChangeUserInfo.as_view()),
    path('', include(router.urls)),
]

Теперь вы можете:

  1. создайте элемент и перечислите все элементы
  2. извлеките, обновите и удалите отдельный элемент

Только для чтения ModelViewSet

ReadOnlyModelViewSet - это набор представлений, который предоставляет только действия list и retrieve, комбинируя GenericViewSet с RetrieveModelMixin и ListModelMixin смешивание.

Например ModelViewSet, ReadOnlyModelViewSet для работы нужны только атрибуты queryset и serializer_class:

from rest_framework.viewsets import ReadOnlyModelViewSet

class ItemReadOnlyViewSet(ReadOnlyModelViewSet):

    serializer_class = ItemSerializer
    queryset = Item.objects.all()

Сводная информация о представлениях API, общих представлениях и наборах представлений

Краткое содержание серии:

  • В первой и второй второй статьях рассказывалось о том, как создавать представления API путем расширения APIView и универсальных представлений, соответственно.
  • В этой статье мы рассмотрели, как создавать представления API с помощью ViewSets.

Для более глубокого понимания представлений мы рассмотрели все основные элементы, но в реальной жизни вы, скорее всего, будете использовать один из следующих:

  1. APIView
  2. конкретные виды
  3. ModelViewSet/ReadOnlyModelViewSet

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

Три конечные точки:

  1. Перечисление всех элементов
  2. Создание нового элемента
  3. Извлечение, обновление и удаление отдельного элемента

Вот как вы можете добиться этого, расширив APIView.:

class ItemsList(APIView):

    def get(self, request, format=None):
        items = Item.objects.all()
        serializer = ItemSerializer(items, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = ItemSerializer(data=request.data)
        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)


class ItemDetail(APIView):

    def get(self, request, pk, format=None):
        item = get_object_or_404(Item.objects.all(), pk=pk)
        serializer = ItemSerializer(item)

        return Response(serializer.data)

    def put(self, request, pk, format=None):
        item = get_object_or_404(Item.objects.all(), pk=pk)
        serializer = ItemSerializer(item, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        item = get_object_or_404(Item.objects.all(), pk=pk)
        item.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Вот как вы делаете то же самое с конкретными общими представлениями:

class ItemsListGeneric(ListCreateAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer


class ItemDetailGeneric(RetrieveUpdateDestroyAPIView):
    queryset = Item.objects.all()
    serializer_class = ItemSerializer

И вот строки, которые вам нужны с ModelViewSet:

class ItemsViewSet(ModelViewSet):
    serializer_class = ItemSerializer
    queryset = Item.objects.all()

Наконец, вот как будут выглядеть соответствующие конфигурации URL-адресов:

# APIView

from django.urls import path
from views import ItemsList, ItemDetail

urlpatterns = [
    path('api-view', ItemsList.as_view()),
    path('api-view/<pk>', ItemDetail.as_view()),
]


# generic views

from django.urls import path,
from views import ItemsListGeneric, ItemDetailGeneric

urlpatterns = [
    path('generic-view', ItemsListGeneric.as_view()),
    path('generic-view/<pk>', ItemDetailGeneric.as_view()),
]


# ViewSet

from django.urls import path, include
from rest_framework import routers
from views import ItemsViewSet

router = routers.DefaultRouter()
router.register(r'viewset', ItemsViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

Заключение

Просмотры DRF представляют собой сложную, запутанную сеть:

DRF Views Overview

Существует три основных способа их использования с несколькими дополнительными возможностями:

  1. Расширение класса APIView
    • можно также использовать функциональные представления с помощью декоратора
  2. Использование универсальных представлений
    • GenericAPIView является основой
    • GenericAPIView можно сочетать с одним или несколькими миксерами
    • Классы Concrete view уже охватывают комбинирование GenericAPIView с миксинами всеми широко используемыми способами
  3. ViewSet объединяет все возможные действия в один класс
    • GenericViewSet является более мощным, чем базовый ViewSet
    • ModelViewSet и ReadOnlyModelViewSet обеспечивают максимальную функциональность при минимальном объеме кода

Все вышеперечисленное обеспечивает простую настройку.

В большинстве случаев вы будете использовать либо APIView, один из конкретных классов представлений, либо (ReadOnly)ModelViewSet. Тем не менее, понимание того, как создаются представления, и преимуществ предков может оказаться полезным, когда вы пытаетесь разработать индивидуальное решение.

Серия просмотров фреймворка Django REST:

  1. Просмотры приложений
  2. Общие представления
  3. Наборы просмотров (эта статья!)
<навигация> Вернуться на верх