在Django中结合GROUP_CONCAT与点赞统计注解处理文章分类的问题
解决Django中同时统计点赞点踩与聚合多对多分类的问题
首先,你的点赞/点踩统计思路是完全可行的,但直接在现有QuerySet上添加分类聚合会遇到一个常见的坑:多对多关联会让QuerySet生成重复行,进而导致Sum统计的点赞数被重复计算。所以我们需要先通过子查询(Subquery)独立统计每篇文章的点赞点踩数,再聚合分类信息,这样就能避免计数错误。
下面分数据库类型给出完整实现方案:
1. 先处理点赞点踩的统计(避免重复计数)
我们用Subquery和OuterRef单独统计每篇文章的点赞、点踩数量,这样不会受多对多分类关联的影响:
from django.db.models import Subquery, OuterRef, IntegerField, Case, When, Sum # 子查询:统计单篇文章的点赞数 upvotes_subquery = ( models.Like.objects.filter(article=OuterRef('pk'), like_state=1) .values('article') .annotate(count=Sum(Case(When(like_state=1, then=1), default=0, output_field=IntegerField()))) .values('count') ) # 子查询:统计单篇文章的点踩数 downvotes_subquery = ( models.Like.objects.filter(article=OuterRef('pk'), like_state=-1) .values('article') .annotate(count=Sum(Case(When(like_state=-1, then=1), default=0, output_field=IntegerField()))) .values('count') )
2. 添加多对多分类的聚合
根据你使用的数据库类型,选择对应的聚合方式:
情况一:使用PostgreSQL
PostgreSQL原生支持StringAgg函数,可以直接用来拼接分类名称:
from django.contrib.postgres.aggregates import StringAgg queryset = queryset.annotate( upvotes_count=Subquery(upvotes_subquery, output_field=IntegerField()), downvotes_count=Subquery(downvotes_subquery, output_field=IntegerField()), # 聚合分类名称,用逗号分隔,去重避免重复分类 category_names=StringAgg('categories__name', delimiter=', ', distinct=True) )
情况二:使用MySQL
MySQL需要用Func封装原生的GROUP_CONCAT函数:
from django.db.models import Func class GroupConcat(Func): function = 'GROUP_CONCAT' template = "%(function)s(%(expressions)s SEPARATOR ', ')" queryset = queryset.annotate( upvotes_count=Subquery(upvotes_subquery, output_field=IntegerField()), downvotes_count=Subquery(downvotes_subquery, output_field=IntegerField()), # 聚合分类名称,用逗号分隔,去重避免重复分类 category_names=GroupConcat('categories__name', distinct=True) )
3. 完整代码示例(以PostgreSQL为例)
from django.db.models import Subquery, OuterRef, IntegerField, Case, When, Sum from django.contrib.postgres.aggregates import StringAgg # 子查询定义 upvotes_subquery = ( models.Like.objects.filter(article=OuterRef('pk'), like_state=1) .values('article') .annotate(count=Sum(Case(When(like_state=1, then=1), default=0, output_field=IntegerField()))) .values('count') ) downvotes_subquery = ( models.Like.objects.filter(article=OuterRef('pk'), like_state=-1) .values('article') .annotate(count=Sum(Case(When(like_state=-1, then=1), default=0, output_field=IntegerField()))) .values('count') ) # 最终QuerySet queryset = queryset.annotate( upvotes_count=Subquery(upvotes_subquery, output_field=IntegerField()), downvotes_count=Subquery(downvotes_subquery, output_field=IntegerField()), category_names=StringAgg('categories__name', delimiter=', ', distinct=True) )
关键说明
- 使用
Subquery的原因:多对多关联会让同一篇文章对应多条分类记录,直接用Sum会重复统计点赞数。子查询是针对单篇文章独立统计,彻底避免了这个问题。 distinct=True:确保同一个分类不会被重复拼接(比如文章和分类的关联如果存在重复记录的话)。- 如果你的分类显示字段不是
name,替换成你实际使用的字段名即可。
内容的提问来源于stack exchange,提问作者Alexey Kislitsin
相关产品推荐
相关产品推荐

