Django DRF序列化列表报'list'无user_project属性错误
问题场景
定义了存在多对一关联关系的Project和Shift两个模型,需要统计指定Project下所有Shift对象的相关指标。
预期实现流程:
- 用户发送携带指定Project参数的GET请求
- 后端调用统计函数计算该项目下所有Shift的统计指标,将结果存入列表后返回
- 列表经序列化后响应给用户
实际发起请求时触发报错:
AttributeError: 'list' object has no attribute 'user_project'
序列化模型实例或QuerySet时无异常,本次待序列化的是普通Python列表,已为返回值编写独立序列化器但始终无法正常运行,判断为序列化器编写错误,同时需要更优的实现方案。
关联代码
models.py
class Project(models.Model): user = models.ForeignKey('User', on_delete=models.CASCADE) task = models.CharField(max_length=128) technical_requirement = models.TextField() customer = models.CharField(max_length=64, blank=True) customer_email = models.EmailField(blank=True) start_of_the_project = models.DateField() salary_per_hour = models.FloatField() project_cost = models.FloatField(blank=True, default=0) project_duration = models.DurationField(blank=True, default=datetime.timedelta(0)) class Shift(models.Model): user_project = models.ForeignKey('Project', on_delete=models.CASCADE) shift_start_time = models.DateTimeField() shift_end_time = models.DateTimeField() shift_duration_time = models.DurationField(blank=True, null=True) salary_per_shift = models.FloatField(blank=True, null=True)
serializers.py(原错误版本)
class ShiftStatisticSerializer(serializers.Serializer): user_project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all(), many=True) number_of_shifts = serializers.IntegerField() number_of_hours = serializers.FloatField() duration_mean = serializers.FloatField() salary_mean = serializers.FloatField() project_cost = serializers.FloatField()
views.py(原错误版本)
class ShiftStatisticView(APIView): queryset = Shift.objects.all() authentication_classes = [authentication.JWTAuthentication] permission_classes = [permissions.IsAuthenticated] def get(self, request): serializer = ShiftStatisticSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) valid = serializer.validated_data.pop('user_project') query = self.get_queryset() statistic = project_services.get_shift_statistics(validated_data=valid[0], queryset=query) serializer = ShiftStatisticSerializer(statistic) return Response(serializer.data) def get_queryset(self): return self.queryset.filter(user_project_id=self.request.data['user_project'])
project_services.py(原错误版本)
def get_shift_statistics(validated_data, queryset): df = pd.DataFrame(list(queryset.values())) number_of_shifts = df["id"].count() number_of_hours = df["shift_duration_time"].sum() / np.timedelta64(1, 'h') duration_mean = df["shift_duration_time"].mean() / np.timedelta64(1, 'h') salary_mean = df["salary_per_shift"].mean() statistics = [validated_data, number_of_shifts, number_of_hours, duration_mean, salary_mean, validated_data.project_cost,] return statistics
报错追踪
第一个报错完整栈信息:
Traceback (most recent call last): File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view return view_func(*args, **kwargs) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/views/generic/base.py", line 84, in view return self.dispatch(request, *args, **kwargs) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch response = self.handle_exception(exc) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception self.raise_uncaught_exception(exc) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception raise exc File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch response = handler(request, *args, **kwargs) File "/Users/Tokio/Developer/ShiftBot/shift/views.py", line 83, in get return Response(serializer.data) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 555, in data ret = super().data File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 253, in data self._data = self.to_representation(self.instance) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 509, in to_representation attribute = field.get_attribute(instance) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 538, in get_attribute relationship = get_attribute(instance, self.source_attrs) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/fields.py", line 97, in get_attribute instance = getattr(instance, attr) AttributeError: 'list' object has no attribute 'user_project'
尝试移除many=True、queryset参数后触发第二个报错:
AssertionError: Relational field must provide aquerysetargument, overrideget_queryset, or set read_only=True.
第二个报错完整栈信息:
Traceback (most recent call last): File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/threading.py", line 1009, in _bootstrap_inner self.run() File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/threading.py", line 946, in run self._target(*self._args, **self._kwargs) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/autoreload.py", line 64, in wrapper fn(*args, **kwargs) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/management/commands/runserver.py", line 134, in inner_run self.check(display_num_errors=True) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/management/base.py", line 487, in check all_issues = checks.run_checks( File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/registry.py", line 88, in run_checks new_errors = check(app_configs=app_configs, databases=databases) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/urls.py", line 14, in check_url_config return check_resolver(resolver) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/urls.py", line 24, in check_resolver return check_method() File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 480, in check for pattern in self.url_patterns: File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/functional.py", line 49, in __get__ res = instance.__dict__[self.name] = self.func(instance) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 696, in url_patterns patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/functional.py", line 49, in __get__ res = instance.__dict__[self.name] = self.func(instance) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 689, in urlconf_module return import_module(self.urlconf_name) File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/importlib/__init__.py", line 126, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "<frozen importlib._bootstrap>", line 1050, in _gcd_import File "<frozen importlib._bootstrap>", line 1027, in _find_and_load File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 688, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 883, in exec_module File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed File "/Users/Tokio/Developer/ShiftBot/ShiftBot/urls.py", line 7, in <module> path('shift/', include('shift.urls')), File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/conf.py", line 38, in include urlconf_module = import_module(urlconf_module) File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/importlib/__init__.py", line 126, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "<frozen importlib._bootstrap>", line 1050, in _gcd_import File "<frozen importlib._bootstrap>", line 1027, in _find_and_load File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 688, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 883, in exec_module File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed File "/Users/Tokio/Developer/ShiftBot/shift/urls.py", line 2, in <module> from shift import views File "/Users/Tokio/Developer/ShiftBot/shift/views.py", line 10, in <module> from shift.serializers import ProjectSerializer, ShiftSerializer, ShiftStatisticSerializer File "/Users/Tokio/Developer/ShiftBot/shift/serializers.py", line 31, in <module> class ShiftStatisticSerializer(serializers.Serializer): File "/Users/Tokio/Developer/ShiftBot/shift/serializers.py", line 32, in ShiftStatisticSerializer user_project = serializers.PrimaryKeyRelatedField() File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 252, in __init__ super().__init__(**kwargs) File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 107, in __init__ assert self.queryset is not None or kwargs.get('read_only'), ( AssertionError: Relational field must provide a `queryset` argument, override `get_queryset`, or set read_only=`True`.
根因分析
- 序列化器默认按字段名从传入实例中取属性/键值,原服务层返回的是按位置排列的普通列表,既没有
user_project属性,也不支持按键名取值,直接触发属性不存在错误。 user_project字段配置错误:单条统计结果对应单个Project,不需要many=True;该字段仅用于序列化输出,不需要反序列化校验逻辑,未设置read_only=True又去掉queryset时,DRF会触发断言报错。- GET请求参数错误从
request.data读取,GET参数存放在request.query_params中,request.data仅用于POST/PUT等带请求体的请求场景。 - 用pandas转DataFrame做聚合属于冗余操作,Django ORM内置的聚合函数性能更高,可避免额外的内存和类型转换开销。
- 原逻辑未校验Project归属,存在越权访问风险,用户可传入任意Project ID查看其他用户的项目统计数据。
修复方案
1. 修正序列化器
输出场景下的关联字段设置read_only=True即可,不需要配置queryset,单条统计对应单个项目,移除many=True:
from rest_framework import serializers class ShiftStatisticSerializer(serializers.Serializer): user_project = serializers.PrimaryKeyRelatedField(read_only=True) number_of_shifts = serializers.IntegerField() number_of_hours = serializers.FloatField() duration_mean = serializers.FloatField() salary_mean = serializers.FloatField() project_cost = serializers.FloatField()
如果不需要返回完整的关联对象序列化结果,仅需要返回项目ID,也可以直接用serializers.IntegerField()定义该字段。
2. 修正服务层逻辑
返回键名和序列化器字段一一对应的字典,不要返回位置列表;优先用ORM聚合替代pandas计算,性能更高:
from django.db.models import Count, Avg, Sum from datetime import timedelta # ORM聚合实现(推荐) def get_shift_statistics(validated_project, queryset): stats = queryset.aggregate( number_of_shifts=Count("id"), total_duration=Sum("shift_duration_time"), duration_mean=Avg("shift_duration_time"), salary_mean=Avg("salary_per_shift") ) # 时间间隔转小时单位 total_hours = stats["total_duration"] / timedelta(hours=1) if stats["total_duration"] else 0 avg_duration_hours = stats["duration_mean"] / timedelta(hours=1) if stats["duration_mean"] else 0 return { "user_project": validated_project, "number_of_shifts": stats["number_of_shifts"], "number_of_hours": round(total_hours, 2), "duration_mean": round(avg_duration_hours, 2), "salary_mean": round(stats["salary_mean"], 2) if stats["salary_mean"] else 0, "project_cost": validated_project.project_cost } # 原pandas逻辑修正版(不推荐) # def get_shift_statistics(validated_data, queryset): # df = pd.DataFrame(list(queryset.values())) # number_of_shifts = df["id"].count() # number_of_hours = df["shift_duration_time"].sum() / np.timedelta64(1, 'h') # duration_mean = df["shift_duration_time"].mean() / np.timedelta64(1, 'h') # salary_mean = df["salary_per_shift"].mean() # return { # "user_project": validated_data, # "number_of_shifts": number_of_shifts, # "number_of_hours": number_of_hours, # "duration_mean": duration_mean, # "salary_mean": salary_mean, # "project_cost": validated_data.project_cost # }
3. 修正视图逻辑
GET参数从request.query_params读取,增加项目归属校验避免越权:
from rest_framework.views import
相关产品推荐
相关产品推荐

