DRF中ModelViewSet关联Post的Comment端点全局校验Post存在问询
问题:DRF ModelViewSet中统一校验父资源(Post)是否存在的方法
我正在基于DRF构建REST API,使用rest_framework.viewsets.ModelViewSet创建端点。定义了Post和Comment模型,每个Comment属于一个Post,已配置两个端点:
router.register(r"posts", views.PostView)router.register(r"(?P<pk>[^/.]+)/comments", views.CommentView)
两者均继承自ModelViewSet,支持CRUD操作。
当前遇到的问题
- GET请求默认返回所有Comment,已重写
get_queryset()获取对应Post的Comment,但Post不存在时返回空列表(期望返回404); - POST请求时已在
perform_create()中用get_object_or_404校验Post存在,但PUT、DELETE、PATCH请求posts/<int:pk>/comments/<int:pk2>时,未校验Post是否存在就允许操作,不符合预期; - 不想重写所有CRUD方法(如
update()、destroy())来做重复的Post存在性校验,希望找到一个能在所有请求触发的统一方法。
附当前CommentView代码:
class CommentView(ModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer lookup_url_kwarg = 'pk2' def get_queryset(self): queryset = Comment.objects.filter(post=self.kwargs.get('pk')) return queryset def perform_create(self, serializer, **kwargs): post = get_object_or_404(Post, pk=self.kwargs.get('pk')) serializer.save(author=self.request.user, post=post)
解决方案
嘿,这个场景我太熟悉了!其实DRF的ViewSet提供了一个完美的钩子来做这种全局前置校验——就是initial()方法,它会在所有请求的处理流程最开始被调用,不管是GET列表、POST创建还是PUT/PATCH/DELETE修改删除,都能在这里统一完成Post的存在性校验,完全不用重复写逻辑到每个CRUD方法里。
第一步:用initial()统一校验Post存在性
在你的CommentView里重写initial()方法,先把URL里的pk对应的Post查出来,不存在直接返回404,而且查出来的Post还能复用在其他方法里,减少重复查询:
from django.shortcuts import get_object_or_404 from rest_framework.viewsets import ModelViewSet class CommentView(ModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer lookup_url_kwarg = 'pk2' def initial(self, request, *args, **kwargs): # 所有请求进来先校验Post是否存在,不存在直接404 self.post = get_object_or_404(Post, pk=self.kwargs.get('pk')) # 别忘了调用父类的initial方法,不然DRF的默认流程会断 super().initial(request, *args, **kwargs) def get_queryset(self): # 直接用已经校验过的self.post,不用再从kwargs里取了 return Comment.objects.filter(post=self.post) def perform_create(self, serializer): # 复用self.post,避免重复查数据库 serializer.save(author=self.request.user, post=self.post)
第二步:重写get_object()确保Comment属于当前Post
这里还要注意一个细节:当你处理单个Comment的请求(比如posts/1/comments/2),默认的get_object()只会根据pk2找Comment,但不会检查这个Comment是不是属于Post 1。如果有人手动构造一个不属于该Post的Comment ID请求,还是能操作成功,这显然不安全。
所以我们要重写get_object(),基于已经过滤过当前Post的get_queryset()结果来查找Comment:
def get_object(self): # 先拿到当前Post下的所有Comment的查询集 queryset = self.get_queryset() # 在这个查询集里找指定的Comment,不存在就返回404 obj = get_object_or_404(queryset, pk=self.kwargs.get(self.lookup_url_kwarg)) # 别忘了检查对象权限(如果你的视图配置了权限类的话) self.check_object_permissions(self.request, obj) return obj
这样做的好处
- 一次校验全局生效:不管什么请求类型,只要进这个视图,都会先过
initial()的校验,Post不存在直接返回404,再也不用在每个方法里重复写get_object_or_404; - 减少数据库查询:
initial()里查一次Post,后续的get_queryset()和perform_create()都能直接用self.post,避免重复查询; - 数据安全有保障:重写
get_object()后,用户只能操作属于当前Post的Comment,防止越权操作其他Post下的评论。
测试一下效果
- 访问
posts/999/comments(999是不存在的Post ID),不管是GET、POST还是其他请求,都会直接返回404; - 访问
posts/1/comments/2如果Comment 2不属于Post 1,会返回404,而不是允许你修改/删除; - 所有CRUD逻辑都保持简洁,不用再写重复的校验代码。
内容的提问来源于stack exchange,提问作者Константин Манойло
相关产品推荐
相关产品推荐

