Роутеры: SimpleRouter и DefaultRouter

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

На предыдущем занятии мы научились использовать классы вьюсетов для комплексной работы с данными таблиц БД, не прописывая отдельные представления для каждого API-маршрута. Но один вопрос остался практически нераскрытым – это работа с маршрутизатором.

Если перейти на страницу официальной документации по Django REST Framework: 5

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

то мы увидим два стандартных класса для определения роутеров:

SimpleRouter и DefaultRouter

Они практически идентичны по функционалу, единственное отличие – это то, что DefaultRouter дополнительно создает корневой маршрут:

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

где отображает все связанные с роутером дочерние маршруты. В нашем случае будет получен следующий JSON-ответ:

{"women":"http://127.0.0.1:8000/api/v1/women/"}

так как был зарегистрирован префикс women для данного роутера. А также позволяет использовать параметр format для API-запросов, например, так:

http://127.0.0.1:8000/api/v1/?format=json

получим «чистый» JSON-ответ, а не HTML-документ для браузера.

Давайте выведем коллекцию router.urls в консоль и посмотрим, из каких маршрутов она состоит:

print(router.urls)

Увидим следующую информацию:

[<URLPattern '^women/$' [name='women-list']>,
 <URLPattern '^women\.(?P<format>[a-z0-9]+)/?$' [name='women-list']>,
<URLPattern '^women/(?P<pk>[^/.]+)/$' [name='women-detail']>,
 <URLPattern '^women/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$' [name='
women-detail']>,
 <URLPattern '^$' [name='api-root']>,
 <URLPattern '^\.(?P<format>[a-z0-9]+)/?$' [name='api-root']>
]

Здесь три группы маршрутов:

  • /api/v1/women/
  • /api/v1/women/pk/
  • /api/v1/

и у каждого маршрута есть свое уникальное имя (параметр name). Это имя можно использовать во фреймворке Django для доступа к соответствующему URL (об этом мы с вами подробно говорили на курсе по Django).

В данном случае имена были сгенерированы автоматически. Здесь префикс «women» - это название модели, взятое из атрибута queryset вьюсета WomenViewSet. А не префикса, указанного при регистрации. Например, если его изменить на «women2»:

router.register(r'women2', WomenViewSet)

то имена маршрутов останутся прежними. Но, при необходимости, мы можем поменять и этот префикс в именах с помощью специального параметра basename при регистрации вьюсета, например:

router.register(r'women', WomenViewSet, basename='men')

Видим, что теперь имена начинаются с men. Кстати, этот параметр (basename) обязателен, если во вьюсете не определен атрибут queryset.

Декоратор @action

Итак, класс роутера, фактически, формирует список URL-маршрутов и связывает их с определенным вьюсетом. Но что делать, если этих маршрутов по умолчанию недостаточно и нужно дополнительно определить свой для того же вьюсета? Разработчики Django REST Framework для этого определили специальный декоратор @action, с помощью которого можно через методы создавать новые, дополнительные маршруты в рамках одного вьюсета.

Давайте, в качестве простого примера, создадим маршрут для отображения списка категорий. Определим в классе WomenViewSet, следующий метод:

class WomenViewSet(mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.ListModelMixin,
                   viewsets.GenericViewSet):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
 
 
    @action(methods=['get'], detail=False)
    def category(self, request):
        cats = Category.objects.all()
        return Response({'cats': [c.name for c in cats]})

Мы здесь через декоратор action указываем список разрешенных методов (в данном случае – это один метод GET), а также тип маршрута. Если указан параметр detail=False, то ожидается работа со списком и маршрут не будет использовать параметр pk – идентификатор записи. Далее следует метод класса с именем category и двумя обязательными параметрами: self и request. Внутри этого метода читаем все записи из таблицы category и формируем самостоятельно JSON-ответ, так как сериализатор класса для этих данных уже не подходит. В результате, роутер сформирует дополнительный URL-адрес вида:

http://127.0.0.1:8000/api/v1/women/category/

Наберем его в браузере и увидим на выходе JSON-строку с именами категорий.

Если же нам нужно работать с конкретной записью, то параметр detail=True и метод category должен определять еще один параметр pk:

    @action(methods=['get'], detail=True)
    def category(self, request, pk=None):
        cats = Category.objects.get(pk=pk)
        return Response({'cats': cats.name})

Теперь в браузере прежний маршрут будет недоступен, а новый имеем вид:

http://127.0.0.1:8000/api/v1/women/1/category/

Обратите внимание, мы здесь идентификатор указываем до фрагмента category, а не после.

Метод get_queryset

Иногда при разработке API нам нужно по URL-запросу выбирать из таблицы БД не все записи, а определенные, иногда по довольно сложным условиям с группировкой и прочим. Как это можно реализовать в рамках нашего проекта? Для этого в каждом классе представления DRF можно переопределить специальный метод get_queryset(), который должен возвращать список отобранных записей. Например, если его записать в виде:

    def get_queryset(self):
        return Women.objects.all()[:3]

то по запросу будут возвращаться только первые три отобранные записи. При этом сам атрибут queryset в классе WomenViewSet можно убрать. Но, так как мы используем роутер, то при регистрации обязательно нужно прописать параметр basename:

router.register(r'women', WomenViewSet, basename='women')

Теперь, при GET-запросе:

http://127.0.0.1:8000/api/v1/women/

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

http://127.0.0.1:8000/api/v1/women/1/

получим ошибку. Дело в том, что наш метод get_queryset() всегда выдает список. Поправим это:

    def get_queryset(self):
        pk = self.kwargs.get("pk")
 
        if not pk: 
            return Women.objects.all()[:3]
 
        return Women.objects.filter(pk=pk)

Теперь, если появляется параметр pk, то мы выдает список из одной конкретной записи, а иначе – список из первых трех. Проверяем, все работает, как и прежде.

Собственный класс роутера (custom router)

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

В качестве примера я непосредственно в файле drfsite/urls.py пропишу новый класс роутера. Конечно, в реальном проекте все определения классов нужно делать в соответствующих файлах. Например, можно создать файл routers.py в пакете women, а затем, импортировать его в модуль urls.py. Но сейчас в учебных целях, думаю, будет достаточно просто определить класс прямо здесь. Он будет следующим:

class MyCustomRouter(routers.SimpleRouter):
    routes = [
        routers.Route(url=r'^{prefix}$',
                      mapping={'get': 'list'},
                      name='{basename}-list',
                      detail=False,
                      initkwargs={'suffix': 'List'}),
        routers.Route(url=r'^{prefix}/{lookup}$',
                      mapping={'get': 'retrieve'},
                      name='{basename}-detail',
                      detail=True,
                      initkwargs={'suffix': 'Detail'})
    ]

По сути, я взял его из документации. Но здесь нет совершенно ничего сложного. Вначале мы наследуемся от класса SimpleRouter, как наиболее простого класса роутера, содержащего необходимый базовый функционал, а затем, внутри нашего класса MyCustomRouter определяем специальный атрибут routes в виде списка из объектов класса Route. Каждый такой класс определяет маршрут, для которого указывается:

  • mapping – связка типа запроса (GET, POST и т.п.) и соответствующего метода вьюсета;
  • name – название маршрута;
  • detail – список или отдельная запись;
  • initkwargs – дополнительные аргументы для коллекции kwargs, которые передаются конкретному представлению при срабатывании маршрута.

В результате, мы с вами определили два маршрута, которые позволяют читать список записей и одну конкретную запись по ее id. Перейдем в браузер, наберем запрос:

127.0.0.1:8000/api/v1/women

Увидим список записей. Обратите внимание, что я здесь не поставил последний слеш, т.к. маршрут в роутере определен без него. То же самое для отображения отдельной записи:

http://127.0.0.1:8000/api/v1/women/1

Увидим только одну конкретную запись с id=1.

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

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