具有合成属性的django ORM中的GROUP BY

时间:2017-01-25 16:04:25

标签: django django-orm

将我的问题转换为更简单的域,假设我继承了一个django应用程序,其数据模型将测试成绩表示为字母(" A +"," A",&# 34; A - "," B +"等),我想将成绩平均值报告为百分比,其中{" A +":100,&# 34; A":95,...}。

玩具模型:

class TestGrade(models.Model):
  student = ForeignKey(Student)
  letter_grade = CharField()
  course = ForeignKey(Course)

(假设改变模型并进行迁移 - 明智的解决方案 - 可能是因为我们希望能够灵活地将不同的字母等级映射应用于数字分数)

要报告这些,我使用明显的查询获得给定课程的成绩,然后根据上面的映射应用案例陈述,例如

grades.annotate(score=Case(When(letter_grade="A+", then=Value(100)), 
                           When(letter_grade="A", then=Value(95)),
                           ...
                           default=Value(0),
                           output_field=IntegerField)))

现在,我想对学生进行GROUP BY并报告他们的平均成绩。不幸的是标准的djangonic方式做了GROUP BY,

grades.values("student_id").annotate(Avg('score'))

KeyError: 'score'

可怕地死去
grades.values("student_id", "score").annotate(Avg('score'))

并不会死得很厉害,但当然它是由id和得分的元组组成的,这不是我想要的。

有没有办法按student_id进行分组并使用合成值的平均值进行注释?

显然,我可以在python中执行此操作,但由于通常的原因,如果合理可行,我想在ORM中执行此操作。

1 个答案:

答案 0 :(得分:1)

你有没有想过把这个逻辑放在模型上?这使得业务逻辑可用于Student对象的任何位置。考虑:

class Student(models.Model)
    ...

    @property
    def average_test_grade(self):
        return self.testgrade_set.all().annotate(
            score=Case(
                When(letter_grade="A", then=Value(100)),
                When(letter_grade="A-", then=Value(90)),
                When(letter_grade="B+", then=Value(89)),
                When(letter_grade="B-", then=Value(80)),
                default=Value(0),
                output_field=IntegerField()
            )
        ).aggregate(Avg('score'))

然后在您的views.py中,您可以使用.prefetch_related()减少查询次数:

students = Student.objects.prefetch_related('testgrade_set').all()

最后在模板中或可能在其他地方:

<ul>
{% for student in students %}
<li>{{ student.average_test_grade }}</li>
{% endfor %}
</ul>