DRF批量更新时出现'QuerySet' object has no attribute 'pk'错误求助
DRF批量更新视图报错:'QuerySet' object has no attribute 'pk'
问题背景
需要实现批量更新Post对象的视图,DRF无内置通用视图,参考官方文档将QuerySet与many=True传入序列化器,但执行PUT/PATCH请求时触发上述错误。
相关代码
视图代码(view.py)
class PostListBulkUpdateApiView(GenericAPIView): serializer_class = PostListUpdateSerializer permission_classes = [permissions.IsAuthenticated, HasPostChangePermission, HasPostReadPermission, HasObjectDeletePermission] def get_serializer(self, *args, **kwargs): if isinstance(kwargs.get('data', {}), list): kwargs['many'] = True return super().get_serializer(*args, **kwargs) def get_queryset(self, ids=None): if ids: return Post.objects.filter(id__in=ids) return Post.objects.all() def put(self, request, *args, **kwargs): return self.update(request) def patch(self, request, *args, **kwargs): return self.update(request) def update(self, request, *args, **kwargs): ids = set() for data in request.data: id = data.get('id', None) if id is not None: ids.add(id) instance = self.get_queryset(ids=ids) serializer = PostListUpdateSerializer(instance=instance, data=request.data, partial=True, many=True) if serializer.is_valid(raise_exception=True): return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
序列化器代码(serializer.py)
class PostListUpdateSerializer(serializers.ModelSerializer): class Meta: model = Post fields = '__all__' read_only_fields = ['id'] def update(self, instance, validated_data): ...
模型代码(model.py)
class Post(models.Model): title = models.CharField( max_length=255, blank=False, null=False, unique=True) body = models.JSONField(blank=False, null=False) def __str__(self): return self.title
错误栈追踪
Environment: Request Method: PATCH Request URL: http://127.0.0.1:8000/api/v2/posts/update Django Version: 3.2.7 Python Version: 3.8.10 Installed Applications: ['whitenoise.runserver_nostatic', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', 'django.forms', 'rest_framework', 'rest_framework_simplejwt', 'rest_framework_simplejwt.token_blacklist', 'django_filters', 'blog', 'blog_media', 'category', 'comment', 'contact', 'user', 'core_api', 'mptt', 'allauth', 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.github', 'allauth.socialaccount.providers.google', 'django_social_share', 'corsheaders', 'debug_toolbar'] Installed Middleware: ['django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware'] Traceback (most recent call last): File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner response = get_response(request) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view return view_func(*args, **kwargs) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/django/views/generic/base.py", line 70, in view return self.dispatch(request, *args, **kwargs) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/views.py", line 509, in dispatch response = self.handle_exception(exc) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/views.py", line 469, in handle_exception self.raise_uncaught_exception(exc) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception raise exc File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/views.py", line 506, in dispatch response = handler(request, *args, **kwargs) File "/home/karunamay/projects/blog_rest_api/blog/views.py", line 240, in patch return self.update(request) File "/home/karunamay/projects/blog_rest_api/blog/views.py", line 251, in update serializer.is_valid(raise_exception=True) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 748, in is_valid self._validated_data = self.run_validation(self.initial_data) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 618, in run_validation value = self.to_internal_value(data) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 666, in to_internal_value validated = self.child.run_validation(item) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 426, in run_validation value = self.to_internal_value(data) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 483, in to_internal_value validated_value = field.run_validation(primitive_value) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/fields.py", line 751, in run_validation return super().run_validation(data) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/fields.py", line 548, in run_validation self.run_validators(value) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/fields.py", line 560, in run_validators validator(value, self) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/validators.py", line 72, in __call__ queryset = self.exclude_current_instance(queryset, instance) File "/home/karunamay/projects/blog_rest_api/venv/lib/python3.8/site-packages/rest_framework/validators.py", line 60, in exclude_current_instance return queryset.exclude(pk=instance.pk) Exception Type: AttributeError at /api/v2/posts/update Exception Value: 'QuerySet' object has no attribute 'pk'
错误原因
Post模型的title字段设置了unique=True,DRF的ModelSerializer会自动为该字段添加UniqueValidator。在批量更新场景下,使用many=True的序列化器时,每个子序列化器接收到的instance是整个QuerySet而非单个Post对象,导致验证器尝试访问instance.pk时抛出属性不存在的错误。同时,DRF默认不会自动将请求数据与QuerySet中的实例按ID匹配,进一步加剧了这个问题。
解决方案
方案1:手动重写字段,移除自动生成的UniqueValidator
修改序列化器,显式定义title字段并清空验证器列表,避免自动添加的验证器干扰,之后手动实现唯一性校验:
class PostListUpdateSerializer(serializers.ModelSerializer): title = serializers.CharField( max_length=255, required=False, # PATCH请求允许字段不传入 validators=[] ) class Meta: model = Post fields = '__all__' read_only_fields = ['id'] def validate_title(self, value): # 排除当前实例后检查标题唯一性 if Post.objects.filter(title=value).exclude(pk=self.instance.pk).exists(): raise serializers.ValidationError("该标题已存在") return value def update(self, instance, validated_data): for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() return instance
方案2:在视图中确保实例与请求数据一一对应
DRF的批量序列化器不会自动按ID匹配请求数据和实例,需要手动排序QuerySet,保证顺序与请求数据一致:
修改视图的update方法:
def update(self, request, *args, **kwargs): # 按ID排序请求数据,确保顺序统一 request_data = sorted(request.data, key=lambda x: x['id']) ids = [item['id'] for item in request_data] # 按ID顺序获取实例,与请求数据一一对应 instances = Post.objects.filter(id__in=ids).order_by('id') serializer = PostListUpdateSerializer( instance=instances, data=request_data, partial=True, many=True ) if serializer.is_valid(raise_exception=True): serializer.save() # 执行批量更新 return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
此方案配合方案1的序列化器修改,能彻底解决实例匹配和验证错误的问题。
补充说明
DRF内置序列化器在批量更新场景下对唯一性验证的支持有限,需要手动处理实例匹配和验证逻辑。如果批量更新需求复杂,也可以考虑拆分请求逐个处理单对象更新,或者使用第三方库简化开发。
内容的提问来源于stack exchange,提问作者K.M
相关产品推荐
相关产品推荐

