Как отсортировать модели Django по сумме рассчитанного свойства? - PullRequest
0 голосов
/ 05 мая 2018

У меня есть две модели, Project и Session. Один проект имеет много сессий, один пользователь имеет много проектов:

class Project(models.Model):
    class Meta:
        ordering = [models.functions.Lower("name")]

    name = models.CharField(max_length=255)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

class Session(models.Model):
    start = models.DateTimeField()
    end = models.DateTimeField()
    timezone = TimeZoneField()
    breaks = models.IntegerField(default=0, validators=[MinValueValidator(0)])
    project = models.ForeignKey(Project, on_delete=models.CASCADE)

    def duration(self):
        # returns minutes in (end - start)

Мне нужен способ получить все проекты для данного пользователя, отсортированные по сумме длительностей во всех его сеансах. * * * * * * * * * * * * * * * * * * * * * * * *

* * * * * * * * * * * * * * * * * то, что я не могу получить эту информацию в одном запросе к базе данных, а session.duration() не является полем базы данных.

Мое текущее решение:

sessions = Session.objects.filter(project__user=self)
groups = [[a, sum([s.duration() for s in b])] for a, b in groupby(
 sessions, key=lambda s: s.project
)]
groups = sorted(groups, key=lambda g: g[1], reverse=True)
return [g[0] for g in groups]

Это получает все соответствующие сеансы в одном запросе, но затем я группирую их по проектам, и это занимает слишком много времени - около секунды, когда существует около 100 проектов. Есть ли способ сделать это, который занимает меньше времени? И в идеале не требует вызова базы данных для каждого проекта?

Я использую Django 2.0.

1 Ответ

0 голосов
/ 05 мая 2018

Вы можете использовать аннотации и агрегацию для достижения этой цели. Во-первых, немного измените модель сеанса, изменив эту строку:

project = models.ForeignKey(Project, on_delete=models.CASCADE)

к этому:

project = models.ForeignKey(Project, related_name='sessions', on_delete=models.CASCADE)

- теперь каждый экземпляр Project будет иметь поле sessions, которое будет содержать набор запросов всех Session s, связанных с этим Project.

Вместо того, чтобы принимать все пользовательские сеансы, как вы делаете сейчас, вы можете взять все пользовательские проекты и циклически просмотреть сеансы каждого проекта, например:

projects = Project.objects.filter(user=self)
for p in projects:
    sessions = p.sessions.all()

Затем вы можете манипулировать набором запросов sessions, аннотируя их полем выражения, например:

from django.db.models import ExpressionWrapper, F, fields

duration_ = ExpressionWrapper(F('end') - F('start'), output_field=fields.DurationField())
sessions = p.sessions.annotate(d=duration_)

В этот момент у каждого члена набора запросов sessions будет поле с именем d, содержащее продолжительность соответствующего Session. Для суммирования длительностей мы можем использовать функцию агрегации наборов запросов Django, например:

from django.db.models import Sum
total = sessions.aggregate(total_duration=Sum('d'))["total_duration"]

То, что мы делаем во 2-й строке, - это создание одного элемента из набора запросов ( «агрегирование» его), добавление всех значений в поле d и присвоение результата поле с именем total_duration. Результат этого выражения:

sessions.aggregate(total_duration=Sum('d'))

- это dict только с одним ключом (total_duration), из которого мы берем значение.

Далее вы можете создать список проектов и продолжительности, а затем отсортировать их по продолжительности, например, как это:

import operator
plist = []
for p in projects:
    sessions = p.sessions.annotate(d=duration_)
    total = sessions.aggregate(total_duration=Sum('d'))["total_duration"]
    # total holds the sum of this project's sessions
    plist.append({'p':p,'total':total})
plist.sort(key=operator.itemgetter('total'))

projects = [item['p'] for item in plist]

Подводя итог:

import operator
from django.db.models import F, Sum, ExpressionWrapper, fields

duration_ = ExpressionWrapper(F('end') - F('start'), output_field=fields.DurationField())
projects = Project.objects.filter(user=self)
plist = []

for p in projects:
    sessions = p.sessions.annotate(d=duration_)
    total = sessions.aggregate(total_duration=Sum('d'))["total_duration"]
    # total holds the sum of this project's sessions
    plist.append({'p':p,'total':total})

plist.sort(key=operator.itemgetter('total'))

projects = [item['p'] for item in plist]

Ссылка: этот ответ , Выражения запросов Джанго , Агрегация Джанго

...