Аннотировать результаты из метода связанной модели на модель Queryset?
Я пытаюсь выяснить лучший / наиболее эффективный способ получить "прогресс" объекта Summary
. Объект Summary
имеет X объектов Grade
- объект Grade
is_complete
когда у него есть Level
выбранный и есть 1 или более связанных Evidence
объектов.
Я пытаюсь привязать этот Summary
"прогресс" к Person
.
models.py
выглядят следующим образом:
class Summary(models.Model):
id = models.BigAutoField(primary_key=True)
person = models.ForeignKey(
Person, on_delete=models.PROTECT, related_name="summaries"
)
finalized = models.BooleanField(default=False)
class Meta:
verbose_name = "Summary"
verbose_name_plural = "Summaries"
def progress(self):
"""Return the progress of the summary."""
grades = self.grades.all()
finished_grades = (
Grade.complete.all().filter(summary=self).count()
)
try:
progress = (finished_grades / grades.count()) * 100
class Grade(models.Model):
id = models.BigAutoField(primary_key=True)
summary = models.ForeignKey(
Summary, on_delete=models.PROTECT, related_name="%(class)ss"
)
level = models.ForeignKey(
Level,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="%(class)ss",
)
class Meta:
verbose_name = "Grade"
verbose_name_plural = "Grades"
@property
def is_complete(self):
if 0 < self.evidences.count() and self.level:
return True
return False
class Evidence(models.Model):
id = models.BigAutoField(primary_key=True)
grade = models.ForeignKey(
Grade, on_delete=models.PROTECT, related_name="%(class)ss"
)
comment = models.TextField()
Моя views.py
выглядит следующим образом:
class PersonListView(ListView):
model = Person
template_name = "app/person_list.html"
context_object_name = "person_list"
def get_queryset(self):
people = Person.objects.all().prefetch_related("summaries", "summaries__grades", "summaries__grades__evidences")
# There should only be one non-finalized summary
# or there will be None
first_summary = Summary.objects.filter(
person__id=OuterRef("id"), finalized=False
)
return people.annotate(
summary_progress=Subquery(first_summary[:1].progress()),
)
Я пытаюсь сделать это за как можно меньшее количество запросов (я думаю, что с prefetch возможно 3-4 запроса в общей сложности?)
В моем шаблоне я пытаюсь упростить получение этого, чтобы я мог сделать что-то простое, когда я просматриваю список людей:
<div class="progress">
{{ student.summary_progress }}
</div>
Приведенный выше код не работает, потому что я пытаюсь аннотировать этот метод .progress()
на кверисет People
. Я не могу понять, как лучше это сделать.
Могло бы что-то подобное подойти вам?
Summary.objects.alias(total_grades=Count("grade"), finished_grades=Count("grade", filter=Q(grade__evidences__isnull=False,level__isnull=False))).annotate(progress=F("finished_grades") / F("total_grades")).
Метод alias позволяет вычислять значения (которые не будут возвращены).
Объект Count возвращает количество связанных записей. Он может использовать Q-фильтры.
Метод annotate позволяет использовать вычисляемые значения, которые будут возвращены.
Объект F позволяет читать столбцы (даже вычисленные) для фильтрации или аннотации
Я придумал решение, похожее на ( ответ Аломбароса), но мне кажется, что я немного доработал его, и я проверил, что оно работает. Используя Django Debug Toolbar, я вижу, что эти данные могут быть получены с помощью ровно одного запроса.
from django.db.models import (
Count,
ExpressionWrapper,
F,
FloatField,
OuterRef,
Q,
Subquery,
Value,
)
class PersonListView(ListView):
model = Person
template_name = "app/person_list.html"
context_object_name = "person_list"
def get_queryset(self):
qs = super().get_queryset()
# Build up the queryset for the subquery
summaries_subquery_qs = Summary.objects.filter(person_id=OuterRef("id"), finalized=False)
summaries_subquery_qs = summaries_subquery_qs.alias(
total_grades=Count("grades"),
finished_grades=Count("grades", filter=Q(
grades__evidences__isnull=False, grades__level__isnull=False
))).annotate(
# ExpressionWrapper allows you to output using a FloatField.
summary_progress=ExpressionWrapper(
# Multiplying by 100 in order to get a percentage value between 0-100
(F("finished_grades") * Value(100) / F("total_grades")),
output_field=FloatField())
)
qs = qs.annotate(
# Now you can annotate that subquery, and "pluck" out the value you want
summary_progress=Subquery(summaries_subquery_qs.values('summary_progress')[:1])
)
return qs
А вот SQL, который генерируется этим запросом:
SELECT "myapp_person"."id",
"myapp_person"."name",
(SELECT ((COUNT(U2."id") FILTER (WHERE (U3."id" IS NOT NULL AND U2."level_id" IS NOT NULL)) * 100) /
COUNT(U2."id")) AS "finished_percentage"
FROM "myapp_summary" U0
LEFT OUTER JOIN "myapp_grade" U2
ON (U0."id" = U2."summary_id")
LEFT OUTER JOIN "myapp_evidence" U3
ON (U2."id" = U3."grade_id")
WHERE (NOT U0."finalized" AND U0."person_id" = "myapp_person"."id")
GROUP BY U0."id"
LIMIT 1) AS "finished_percentage"
FROM "myapp_person"