【问题标题】:Trouble with Django annotation with multiple ForeignKey references带有多个 ForeignKey 引用的 Django 注释问题
【发布时间】:2021-05-06 19:42:35
【问题描述】:

我正在努力使用注释,但没有找到帮助我理解的示例。以下是我的模型的相关部分:

    class Team(models.Model):
        team_name = models.CharField(max_length=50)
    
    class Match(models.Model):
        match_time = models.DateTimeField()
        team1 = models.ForeignKey(
            Team, on_delete=models.CASCADE, related_name='match_team1')
        team2 = models.ForeignKey(
            Team, on_delete=models.CASCADE, related_name='match_team2')
        team1_points = models.IntegerField(null=True)
        team2_points = models.IntegerField(null=True)

我想最终得到的是 Teams 对象上的注释,它可以为我提供每个团队的总分。有时,一个团队是match.team1(所以他们的积分在match.team1_points),有时他们是match.team2,他们的积分存储在match.team2_points

这是我得到的最接近的结果,大概尝试了一百次左右:

teams = Team.objects.annotate(total_points = 
Value(
  (Match.objects.filter(team1=21).aggregate(total=Sum(F('team1_points'))))['total'] or 0 + 
  (Match.objects.filter(team2=21).aggregate(total=Sum(F('team2_points'))))['total'] or 0, 
  output_field=IntegerField())
)

这很好用,但(当然)为 pk=21 的团队注释 total_points 到查询集中的每个团队。如果有更好的方法来解决这一切,我很乐意看到它,但除此之外,如果你能告诉我如何将那些“21”值转化为对外部团队 pk 的参考,我认为这会奏效吗?

编辑:我最终结合使用 elyas 的答案和注释原始 SQL 语句来解决我的问题。我无法阻止普通注释从查询集中删除非唯一分数,但原始 SQL 似乎可以工作。

这是原始注释:

teams = Team.objects.raw('select id, sum(points) as total_points from (select team1_id as id, team1_points as points from leagueman_match union all select team2_id as id, team2_points as points from leagueman_match) group by id order by total_points desc;')

【问题讨论】:

    标签: django django-queryset django-orm


    【解决方案1】:

    注释 QuerySet 的另一种解决方案可能是使 total_points 成为 Team 模型的 @property(取决于用例):

    from django.db.models import Case, Q, Sum, When
    
    
    class Team(models.Model):
        team_name = models.CharField(max_length=50)
    
        @property
        def total_points(self):
            return Match.objects.filter(Q(team1=self.id) | Q(team2=self.id)).aggregate(
                total_points=Sum(Case(
                    When(team1=self.id, then='team1_points'),
                    When(team2=self.id, then='team2_points')
                ))
            )['total_points']
    

    缺点是它不能用于后续的 QuerySet 操作,例如.values(), .order_by().

    Django 还有一个@cached_property decorator,它将在第一次调用时缓存属性的输出。

    尝试了其他解决方案

    最初我认为您可以利用 Team 模型中的反向关系 match_team1match_team2 来生成一个简单的注释:

    teams = Team.objects.annotate(
        total_points=(
            Sum('match_team1__team1_points', distinct=True)
            + Sum('match_team2__team2_points', distinct=True)
        )
    )
    

    不幸的是,此解决方案在处理重复项时遇到了困难。 distinct=True 参数消除了同一场比赛的分数被多次相加的问题。但它引入了一个不同的问题,即得分相同的不同比赛将被排除在外。

    【讨论】:

    • 感谢您的参与。我认为反向关系的魔力也应该有所帮助,但这似乎不起作用......每个团队的 total_points 值我都得到“无”。如果我只运行这个:```` teams = Team.objects.annotate(total_points=( Sum('match_team1__team1_points'))) ```` ...那么匹配 match_team1 的团队将获得他们的积分。我没有(但可以,如果有帮助的话)验证他们以这种方式找到的总分是准确的。
    • 哎呀。评论格式(和时间限制?)让我明白了。对不起,丑陋的。
    • 我猜有一些小进步。以下注释“几乎”获得了所有分数。剩下的问题是,如果一支球队在多场比赛中得分相同,那么重复的分数将被忽略,并且报告的总分太低。 teams = Team.objects.annotate(t1_points=Coalesce(Sum('match_team1__team1_points', distinct=True),0),t2_points=Coalesce(Sum('match_team2__team2_points', distinct=True),0),total_points = F('t1_points') + F('t2_points')).order_by('-total_points')
    • 很遗憾,我无法复制您返回 None 的错误。我发布的代码适用于我创建的用于测试的小型数据集(它涵盖了各种组合,以及_points 字段何时为None)。我不太明白为什么它不适合你。
    • 这个适合你的子集 - teams = Team.objects.annotate(total_points=( Sum('match_team1__team1_points'))) - 返回正确的总数。
    【解决方案2】:

    也许我会建议重构您的数据模型。即使您找到了解决此特定问题的方法,您也可能想提前考虑。 这是一个解决方案:

    class Team(models.Model):
        team_name = models.CharField(max_length=50)
    
    class Match(models.Model):
        match_time = models.DateTimeField()
        team1 = models.ForeignKey(
            Team, on_delete=models.CASCADE, related_name='match_team1')
        team2 = models.ForeignKey(
            Team, on_delete=models.CASCADE, related_name='match_team2')
    
    class Points(models.Model):
        match = models.ForeignKey(
            Match, on_delete=models.CASCADE, related_name='match')
        team = models.ForeignKey(
            Team, on_delete=models.CASCADE, related_name='team')
        points = models.IntegerField(null=True)
    

    有了这个,您可以轻松总结任何球队的积分,也可以按比赛进行过滤。

    【讨论】:

    • 谢谢。我希望我可以改变模型,但在这一点上,我被困在我必须使用的东西上。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-04
    • 1970-01-01
    • 1970-01-01
    • 2011-01-10
    • 1970-01-01
    相关资源
    最近更新 更多