Django-RESTful Api之ViewSets和Routers
这篇讲什么
- ViewSet 把「按 HTTP 方法写函数」换成「按动作写方法」,Router 再用一行
router.register()把列表、详情、增删改查的路由和反解名字全生成出来。 - 这篇把 ViewSet 与 Mixin 的关系、不用 Router 时的手动映射、
@action自定义路由、反解与超链接逐个跑通,末尾列了实测踩到的新手卡点。实测环境 Django 5.2.19 + DRF 3.18.3。
目的
- ViewSet 是比
APIView抽象程度更高的一层:它不再按 HTTP 方法写函数,而是按动作(list / create / retrieve / update / partial_update / destroy)写,路由由 Router 自动生成。 - 一个资源从「列表 + 详情 + 增删改查」到「URL + 反解名字」全部由一行
router.register()搞定,代码量最少。 - 本篇实测环境:Django 5.2.19 + DRF 3.18.3。
ViewSet 与 Mixin 的关系
-
实测继承链:
1
2
3
4ModelViewSet 的 MRO: ['ModelViewSet', 'CreateModelMixin', 'RetrieveModelMixin',
'UpdateModelMixin', 'DestroyModelMixin', 'ListModelMixin',
'GenericViewSet', 'ViewSetMixin', ...]
ReadOnlyModelViewSet 的 Mixin: ['ListModelMixin', 'RetrieveModelMixin', ...]- 也就是说
ModelViewSet= 五个 CRUD Mixin +GenericViewSet,不用自己写任何方法就能全套增删改查。 - 只读接口用
ReadOnlyModelViewSet(只挂 list + retrieve,实测 POST 会 405)。
- 也就是说
1 | from rest_framework import permissions, viewsets |
手动映射方法(不用 Router 时)
-
ViewSet 的入口是
as_view({HTTP方法: 动作}):urls.py 1
2
3
4
5
6
7
8
9student_list = StudentViewSet.as_view({"get": "list", "post": "create"})
student_detail = StudentViewSet.as_view({
"get": "retrieve", "put": "update", "patch": "partial_update", "delete": "destroy",
})
urlpatterns = format_suffix_patterns([
re_path(r"^students/$", student_list, name="studentList"),
re_path(r"^students/(?P<pk>\d+)", student_detail, name="studentDetail"),
]) -
一个实测细节:
actions这个映射表是as_view()时才生成的,直接实例化 ViewSet 拿不到:1
2hasattr(StudentViewSet, "actions"): False | 直接实例化 hasattr: False
as_view({"get": "list"}) 之后才有: True | 内容: {'get': 'list'}调试时想看某个 ViewSet 支持哪些动作,就
SomeViewSet.as_view({"get": "list"}).actions这样看。
Router:一行注册全套路由
1 | from django.urls import include, path |
-
实测 Router 生成的路由名与形状:
1
2
3
4
5router 生成的名字: ['grade-detail', 'grade-list', 'student-detail', 'student-list',
'user-detail', 'user-list']
list 路由: ^vs/students/$
detail 路由: ^vs/students/(?P<pk>[^/.]+)/$
带后缀路由: ^vs/students/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$DefaultRouter比SimpleRouter多做两件事:生成 API 根视图、自动加格式后缀路由。- 主键那段用
[^/.]+(排除点号),就是为了把.json让给格式后缀。
-
API 根视图在哪(实测)
1
2
3
4/api/vs/ : 404
/api/ : 200 | {"vs/students": "http://testserver/api/vs/students/",
"vs/grades": "http://testserver/api/vs/grades/",
"vs/users": "http://testserver/api/vs/users/"}- 根视图挂在
include(router.urls)的位置上,不是挂在 register 的前缀上;所以router.register(r"vs/students", ...)+path("", include(router.urls))+path("api/", include("school.urls"))的根是/api/。
- 根视图挂在
-
basename什么时候必须写(实测)1
2
3显式 basename 可用: ['g-list', 'g-detail']
不写 basename 且没有 queryset: AssertionError `basename` argument not specified, and
could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute.- 用
viewsets.ViewSet(不带queryset)或动态get_queryset()时,Router 推不出名字,必须显式register("acts", ActViewSet, basename="act")。
- 用
-
结尾斜杠:GET 会 301,POST 直接抛异常(实测)
1
2
3
4
5
6
7
8/api/vs/students GET -> 301 | Location=/api/vs/students/
/api/vs/students/ GET -> 200
/api/vs/students/1 GET -> 301 | Location=/api/vs/students/1/
POST 无斜杠直接抛: RuntimeError: You called this URL via POST, but the URL doesn't end in a
slash and you have APPEND_SLASH set. Django can't redirect to the slash URL while
maintaining POST data. Change your form to point to testserver/api/vs/students/
(note the trailing slash), or set APPEND_SLASH=False in your Django settings.这是 Router 最容易踩的坑:Router 生成的路由都带结尾斜杠,客户端漏了斜杠时 GET 能被
APPEND_SLASH救回来(301),POST/PUT 救不回来(301 会丢请求体),直接 500。前端调用一律写全斜杠。
@action:给 ViewSet 加自定义路由
1 | from rest_framework.decorators import action |
-
实测 Router 为它生成的三条路由:
1
2
3('act-stats', '^acts/stats/$')
('act-mark', '^acts/(?P<pk>[^/.]+)/mark/$')
('act-extra-info', '^acts/(?P<pk>[^/.]+)/extra_info/$')detail=False→ 挂在列表位置(acts/stats/),detail=True→ 挂在详情位置(acts/<pk>/xxx/)。url_path="mark"可以改 URL 上的名字;方法名里的下划线在路由名里会被转成连字符(extra_info→ 路由名act-extra-info),反解时要用带连字符的名字。@action里methods不写默认只有get。
反解与超链接
-
Router 生成的名字带
-list/-detail后缀,反解写法:1
2
3
4from django.urls import reverse
reverse("school:student-list") # /api/vs/students/
reverse("school:student-detail", kwargs={"pk": 8}) # /api/vs/students/8/- 应用
urls.py里写了app_name时必须带命名空间(这条在《Django-视图》和《Django-RESTful-Api设计风格》里都实测过:不写就NoReverseMatch,超链接序列化器还会整个接口 500)。
- 应用
-
想在 ViewSet 里拿到当前请求的绝对地址,用
self.get_request_url()/serializer.context["request"],别自己拼域名。
新手卡点清单
-
都是实测踩过的:
卡点 症状(实测原文) 正确做法 客户端 POST 漏结尾斜杠 RuntimeError: You called this URL via POST, but the URL doesn't end in a slash...URL 一律写全 /students/viewsets.ViewSet注册时不写 basenameAssertionError: basename argument not specified ...register(prefix, ViewSet, basename="x")直接实例化 ViewSet 看 actionsAttributeError: 'StudentViewSet' object has no attribute 'actions'SomeViewSet.as_view({"get": "list"}).actions以为 API 根在注册前缀下 实测 /api/vs/404,根在/api/根视图在 include(router.urls)的位置只读接口用 ModelViewSet写操作全部开放 用 ReadOnlyModelViewSet@action方法名带下划线却按下划线反解NoReverseMatch路由名里下划线变连字符( act-extra-info)在 @action(detail=True)里不接pk拿不到对象 签名写 def mark(self, request, pk=None)Router + app_name后超链接序列化器 500ImproperlyConfigured Could not resolve URL ... "student-detail"extra_kwargs里写view_name="school:student-detail"
参考
- Django REST framework:ViewSets:
ViewSet/ModelViewSet/ReadOnlyModelViewSet与@action。 - Django REST framework:Routers:
SimpleRouter、DefaultRouter、basename、路由命名规则。 - Django REST framework:ViewSets(extra actions):
detail/url_path/methods参数。