You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 17:44:54