Django Rest Framework 实现多对多关联模型嵌套子路由访问方法
问题根源梳理
- 当前视图继承的是
ReadOnlyModelViewSet,按照DRF的内置定义,该基类仅提供列表查询、单条详情查询两个只读接口,本身不支持POST/PATCH/DELETE等写操作;如果你之前观测到接口支持PATCH,说明实际运行的视图代码和你贴出的版本不一致,需要先修正视图基类才能拿到完整操作能力 - DRF 默认的路由注册逻辑,只会为当前模型本身生成资源路径,不会自动生成跨模型关联的嵌套路径(也就是
formations/{id}/courses/这类关联接口),需要通过嵌套路由配置或自定义接口动作来实现
实现全功能嵌套关联接口的方案
第一步:修正基础视图能力
把原有只读视图替换为具备完整CRUD能力的视图基类:
# views.py 基础视图修正 from rest_framework import viewsets, mixins # 如果需要全部增删改查能力,直接用ModelViewSet class FormationView(viewsets.ModelViewSet): queryset = Formation.objects.all() serializer_class = FormationSerializer class CourseView(viewsets.ModelViewSet): queryset = Course.objects.all() serializer_class = CourseSerializer
如果不需要开放删除接口,可以通过mixins组合自定义能力,比如继承
ListModelMixin/RetrieveModelMixin/UpdateModelMixin/CreateModelMixin,不包含DestroyModelMixin即可。
第二步:配置嵌套关联接口
方案一:使用嵌套路由库(低代码、推荐)
通过嵌套路由的能力,自动生成符合REST规范的关联接口路径,复用原有Course视图的全部逻辑,不需要重复写接口代码:
- 先安装
drf-nested-routers依赖 - 在urls.py中补充嵌套路由配置:
# urls.py 嵌套路由配置 from rest_framework_nested import routers router = routers.DefaultRouter() router.register('formations', views.FormationView) router.register('courses', views.CourseView) # 为Formation资源下的关联Course子资源生成路由 formation_course_router = routers.NestedDefaultRouter(router, 'formations', lookup='formation') # 绑定子资源路由到已有的CourseView,访问路径为/api/formations/{formation_pk}/courses/ formation_course_router.register('courses', views.CourseView, basename='formation-courses') urlpatterns = [ path('api/', include(router.urls)), path('api/', include(formation_course_router.urls)), ]
配置完成后,嵌套路径会自动继承CourseView的全部能力:
- 支持
GET /api/formations/{id}/courses/:返回当前Formation关联的所有Course列表 - 支持
POST /api/formations/{id}/courses/:新增Course并绑定到当前Formation - 支持
GET /api/formations/{id}/courses/{course_id}/:查询当前Formation下的指定关联Course详情 - 支持
PUT/PATCH /api/formations/{id}/courses/{course_id}/:修改当前Formation下的指定Course信息 - 支持
DELETE /api/formations/{id}/courses/{course_id}/:删除指定Course(可根据业务调整为仅解除关联关系)
方案二:自定义ViewSet动作(无额外依赖)
如果不想引入第三方依赖,可以直接在FormationView中通过自定义action实现关联接口,不需要修改原有路由结构:
# 在FormationView中补充自定义关联接口 from rest_framework.decorators import action from rest_framework.response import Response class FormationView(viewsets.ModelViewSet): queryset = Formation.objects.all() serializer_class = FormationSerializer # 实现当前Formation关联Course的列表查询、新增接口,路径为formations/{id}/courses @action(detail=True, methods=['get', 'post'], url_path='courses') def manage_course_list(self, request, pk=None): formation = self.get_object() # 处理GET请求:返回关联Course列表 if request.method == 'GET': courses = formation.courses.all() return Response(CourseSerializer(courses, many=True).data) # 处理POST请求:新建Course并绑定到当前Formation serializer = CourseSerializer(data=request.data) serializer.is_valid(raise_exception=True) course = serializer.save() formation.courses.add(course) return Response(serializer.data, status=201) # 实现单个关联Course的操作接口,路径为formations/{id}/courses/{course_id} @action(detail=True, methods=['get', 'put', 'patch', 'delete'], url_path='courses/(?P<course_pk>[^/.]+)') def manage_single_course(self, request, pk=None, course_pk=None): formation = self.get_object() try: # 仅查询当前Formation关联的Course,避免跨资源越权 course = formation.courses.get(pk=course_pk) except Course.DoesNotExist: return Response(status=404) # 单个Course查询 if request.method == 'GET': return Response(CourseSerializer(course).data) # 单个Course更新 if request.method in ['PUT', 'PATCH']: serializer = CourseSerializer(course, data=request.data, partial=request.method=='PATCH') serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) # 单个Course删除:可根据业务调整为仅解除关联(formation.courses.remove(course))或物理删除(course.delete()) if request.method == 'DELETE': formation.courses.remove(course) return Response(status=204)
补充注意事项
- 原有
FormationSerializer中的courses = CourseSerializer(many=True)字段,建议根据业务配置read_only=True,避免和嵌套路由的写入逻辑产生冲突 - 多对多关联操作时,要明确区分「解除Course和Formation的绑定关系」和「物理删除Course对象」的业务逻辑,不要混淆
内容的提问来源于stack exchange,提问作者ABDERRAHMANE HAMMIA
相关产品推荐
相关产品推荐

