Django 1.11:按用户分组并基于首次注解计算总阅读时长
Django 1.11 按用户汇总阅读会话总时长问题解决
这个问题我之前维护Django 1.x项目时也碰到过,核心是没搞清楚QuerySet里values和annotate的执行顺序逻辑,先给你拆解错误原因,再给两种可行的解决方法:
错误原因分析
你遇到的KeyError: 'duration'是Django QuerySet的特性导致的:
当你调用.values('user')时,它会重置当前QuerySet要查询的字段集合,只保留user这个关联字段,之前通过annotate(duration=...)生成的临时计算字段会被直接丢弃。后续再调用.annotate(total_duration=Sum('duration'))时,QuerySet里已经没有duration字段了,自然就抛出KeyError。
而如果把duration加到values参数里,分组逻辑就变成了按user + duration组合分组,这会把同一个用户的不同时长的会话分开统计,完全不符合你“按用户汇总所有会话时长”的需求。
可行解决方法
方法一:直接分组汇总(推荐,更高效)
不需要单独给每个会话计算时长,直接在分组时用Sum包裹Func函数,一次性计算每个用户的总时长。这样跳过了中间临时字段的生成,避免了values过滤字段的问题:
from django.db.models import F, Func, Sum usernames = ['user1', 'user2'] # 按用户分组,直接汇总所有会话的时长总和 queryset = ReadingSession.objects.filter(user__username__in=usernames)\ .values('user__username', 'user_id') # 用username或user_id作为分组依据,方便后续查看结果 .annotate( total_duration=Sum( Func(F('ended_at'), F('started_at'), function='age') ) ) # 遍历输出结果 for record in queryset: print(f"用户 {record['user__username']} 的总阅读时长:{record['total_duration']}")
方法二:保留临时字段到汇总阶段
如果你确实需要先拿到每个会话的单独时长,再进行汇总,可以调整QuerySet的执行顺序,把最终的values(指定输出字段)放到最后,确保duration字段在汇总时仍然存在:
from django.db.models import F, Func, Sum usernames = ['user1', 'user2'] queryset = ReadingSession.objects.filter(user__username__in=usernames)\ .annotate(duration=Func(F('ended_at'), F('started_at'), function='age'))\ .values('user__username', 'user_id') # 先指定分组依据 .annotate(total_duration=Sum('duration'))\ .values('user__username', 'total_duration') # 最后只输出需要的字段 print(queryset)
注意事项
- 这里用到的
age是PostgreSQL的内置函数,如果你用的是其他数据库(比如MySQL),需要替换成对应计算时间差的函数(比如MySQL的TIMESTAMPDIFF)。 - Django会把数据库返回的时间间隔类型转成Python的
datetime.timedelta对象,你可以直接用它的方法(比如total_seconds())转换成更易读的格式。
内容的提问来源于stack exchange,提问作者Watchoutman
相关产品推荐
相关产品推荐

