使用queryset将筛选器应用于嵌套的反向外键关系



上下文

我正在尝试根据反向外键属性的值筛选对象列表。我能够在视图级别解决它,但是,其他使用ORM功能解决问题的尝试会导致额外的查询。

我想要的结果是用所有对象进行queryset,但相关的fkey对象会在每个对象中进行过滤。

示例模型

class Student(models.Model):
name = models.CharField(max_length=128)

class Subject(models.Model):
title = models.CharField(max_length=128)
class Grade(models.Model):
student = models.ForeignKey("Student", related_name="grades", on_delete=models.CASCADE)
subject = models.ForeignKey("Subject", related_name="grades", on_delete=models.CASCADE)
value = models.IntegerField()

给定固定装置

+------+------------------------+
| name | subject  | grade_value |
+------+----------+-------------+
| beth | math     | 100         |
| beth | history  | 100         |
| beth | science  | 100         |
| mark | math     | 90          |
| mark | history  | 90          |
| mark | science  | 90          |
| mike | math     | 90          |
| mike | history  | 80          |
| mike | science  | 80          |
+------+----------+-------------+

预期结果

我想呈现一个学生列表,,但只包括数学历史成绩

例如,也许我想要一份学生名单,但只包括他们成绩的一个子集:

GET students/?subjects=math,history

经过过滤的可能在请求中提供,也可能是硬编码的。如果可能的话,我们可以将其排除在这个问题的范围之外,并假设过滤参数固定为mathhistory

{
"students": [
{
"name": "beth",
"grades": [
{"subject": "math", "grade": 100 },
{"subject": "history", "grade": 100 },
// Exclude one or more grades - eg.
// science grade not included
]
},
...
]
}

尝试的解决方案

简单筛选器

只是过滤。我想这会过滤所有学生,他们的成绩和列表中的科目有关,仅此而已。

queryset = Students.objects.all()
.prefetch_related("grades")
.filter(grades__subject__in=["math", "history"])
)
# all grades for each student eg.
...
"grades": [
{"subject": "math", "grade": 100 },
{"subject": "history", "grade": 100 },
{"subject": "science", "grade": 100 },
]
...

子查询

我不太了解子查询是如何工作的,但使用我尝试过的一些例子:

subjects = Subject.objects.filter(
name__in=["math", "history"]
) 
queryset = Students.objects.all()
.prefetch_related("grades")
.filter(grades__subject__name__in=Subquery(subjects.values("name")))

还有另一种变体:

grades = Grades.objects.filter(
student_id=OuterRef("id"), subject__name__in=["math", "history"]
) 
queryset = Students.objects.all()
.prefetch_related("grades")
.filter(grades__pk__in=Subquery(grades.values("pk)))

两位同学都以全部成绩返校。

变通解决方案

此解决方案使用python过滤成绩。它可以工作,但我更愿意让它与查询集一起工作

# in view:
serializer = StundentSerializer(queryset, many=True)
response_data = serializer.data
for student in response_data:
student.grades = [g for g in students.grades if g["subject"] in ["math", "history"]]
...
# return response_data

您可以使用Prefetch对象:https://docs.djangoproject.com/en/3.0/ref/models/querysets/#django.db.models.Prefetch

例如:

qs = Students.objects.all().prefetch_related(
Prefetch('grades', queryset=Grade.objects.filter(subject__title__in=["math", "history"])
)

qs[0].grades.all()现在只有数学和历史成绩。

您可以选择向Prefetch提供to_attr='math_history_grades'参数,这样您就可以通过以下方式访问成绩:qs[0].math_history_grades.all()

最新更新