Context
I am trying to sort a list of objects based on the value of a reverse foreign key relationship. I have partial solution, but filter used creates an inner join that drops objects without the relationship.
The outcome I want to create sorted queryset with all objects, threating "null" reverse relationships as if the related value was null.
Models
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()
Fixtures
+------+------------------------+
| name | subject | grade_value |
+------+----------+-------------+
| beth | math | 100 |
| beth | history | 100 |
| mark | math | 90 |
| mark | history | 90 |
| mike | history | 80 |
+------+----------+-------------+
Desired Outcome
When sorting students by their "history" grade, I want to get [ beth (100), mark (90), mike (80) ]
Now let's say mark was sick at home and missed the math exam.
When sorting students by their "math" grade, I want to get [ beth (100), mark (90), mike (null) ]
Instead, for the sorted by math grade returns [ beth (100), mark (90) ].
Attempted Solution
I know I could fetch all and rejoin results, but I looking for an efficient ORM queryset solution if possible.
>>> Student.objects.filter(grades__subject__title="history").order_by("grades__value")
<Student: Beth>, <Student: Mark>, <Student: Mike>
>>> Student.objects.filter(grades__subject__title="math").order_by("grades__value")
<Student: Beth>, <Student: Mark>
Update
I tried using a union as suggested but I am not sure why it causes related look up to fail:
>>> Student.objects.filter(grades__subject__title="math")\
.union(Student.objects.all())\
.order_by("grades__value")
...
django.core.exceptions.FieldError: Cannot resolve keyword 'value' into field. Choices are: grades, id, name
Solution
Validation below using solution by @schillingt.
grades = Grade.objects.filter(
student_id=OuterRef('id'), subject__title="math"
)
students = Student.objects.annotate(
subject_grade=Subquery(grades.values('value')[:1])
)
>>> [f"{s.name}: {s.subject_grade}" for s in students.order_by("subject_grade")]
['mike: None', 'mark: 90', 'beth: 100']
>>>
>>> [f"{s.name}: {s.subject_grade}" for s in students.order_by("-subject_grade")]
['beth: 100', 'mark: 90', 'mike: None']
>>>