September 11, 2019

DRF - ViewSets & Routers

Платформа REST включает в себя абстракцию для работы с ViewSets, которая позволяет разработчику сконцентрироваться на моделировании состояния и взаимодействий API и оставить обработку URL-адреса автоматически, основываясь на общих соглашениях.

Классы ViewSet - это почти то же самое, что и классы View, за исключением того, что они предоставляют такие операции, как чтение или обновление, а не обработчики методов, такие как get или put.

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

Рефакторинг для использования ViewSets

Давайте возьмем наш текущий набор представлений и преобразуем их в наборы представлений.

Прежде всего давайте проведем рефакторинг наших представлений UserList и UserDetail в один UserViewSet. Мы можем удалить два представления и заменить их одним классом:

from rest_framework import viewsets

class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    Этот viewset автоматически обеспечивает API для `list` and `detail`.
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

Здесь мы использовали класс ReadOnlyModelViewSet для автоматического предоставления стандартных операций «только для чтения». Мы по-прежнему устанавливаем атрибуты queryset и serializer_class точно так же, как мы это делали, когда использовали обычные представления, но нам больше не нужно предоставлять одну и ту же информацию двум отдельным классам.

Далее мы заменим классы представления SnippetList, SnippetDetail и SnippetHighlight. Мы можем удалить три представления и снова заменить их одним классом.

from rest_framework.decorators import action
from rest_framework.response import Response

class SnippetViewSet(viewsets.ModelViewSet):
    """Этот viewset предоставляет `list`, `create`, `retrieve`,`update` и `destroy` функции.Дополнительно мы добавили `highlight`."""
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]

    @action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

На этот раз мы использовали класс ModelViewSet, чтобы получить полный набор операций чтения и записи по умолчанию.

Обратите внимание, что мы также использовали декоратор @action для создания настраиваемого действия с именем highlight. Этот декоратор может использоваться для добавления любых пользовательских конечных точек, которые не вписываются в стандартный стиль создания / обновления / удаления.

Пользовательские действия, которые используют декоратор @action, будут отвечать на запросы GET по умолчанию.

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

URL-адреса для пользовательских действий по умолчанию зависят от имени самого метода. Если вы хотите изменить способ создания URL, вы можете включить url_path в качестве аргумента ключевого слова decorator.

Привязка ViewSets к URL явно

Методы-обработчики связываются с действиями только тогда, когда мы определяем URLConf.

Чтобы увидеть, что происходит внутри, давайте сначала явно создадим набор представлений из наших ViewSets. В файле snippets/urls.py мы связываем наши классы ViewSet в набор конкретных представлений.

from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers

snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
    'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
    'get': 'list'
})
user_detail = UserViewSet.as_view({
    'get': 'retrieve'
})

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

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

urlpatterns = format_suffix_patterns([
    path('', api_root),
    path('snippets/', snippet_list, name='snippet-list'),
    path('snippets/<int:pk>/', snippet_detail, name='snippet-detail'),
    path('snippets/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
    path('users/', user_list, name='user-list'),
    path('users/<int:pk>/', user_detail, name='user-detail')
])

Использование роутеров

Поскольку мы используем классы ViewSet, а не классы View, нам на самом деле не нужно самостоятельно разрабатывать URL-адреса. Соглашения о подключении ресурсов к представлениям и URL-адресам могут обрабатываться автоматически с использованием класса Router. Все, что нам нужно сделать, это зарегистрировать соответствующие наборы представлений на маршрутизаторе и позволить ему сделать все остальное.

Вот наш перемонтированный файл snippets / urls.py.

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views

# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)

# The API URLs are now determined automatically by the router.
urlpatterns = [
    path('', include(router.urls)),
]

Регистрация наборов с помощью маршрутизатора аналогична предоставлению urlpattern. Мы включаем два аргумента - префикс URL для представлений и сам набор.

Используемый нами класс DefaultRouter также автоматически создает для нас корневое представление API, поэтому теперь мы можем удалить метод api_root из нашего модуля представлений.

Компромиссы между представлениями и наборами

Использование viewsets может быть действительно полезной абстракцией. Это помогает обеспечить согласованность URL-соглашений по всему API, минимизирует объем кода, который необходимо написать, и позволяет вам сосредоточиться на взаимодействиях и представлениях, предоставляемых вашим API, а не на особенностях URL-адреса conf.

Это не значит, что это всегда правильный подход. Существует аналогичный набор компромиссов, которые следует учитывать при использовании представлений на основе классов вместо представлений на основе функций. Использование viewsets менее очевидно, чем создание ваших представлений по отдельности.