Курс по 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 в консоль и посмотрим, из каких маршрутов она состоит:
Увидим следующую
информацию:
[<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