Django уменьшает количество запросов (связь M2M со сквозной моделью)

Я хотел бы уменьшить количество подобных запросов. Вот мои модели:

class Skill(models.Model):
    name = models.TextField()

class Employee(models.Model):

    firstname = models.TextField()
    skills = models.ManyToManyField(Skill, through='SkillStatus')

    def skills_percentage(self):
        completed = 0
        total = 0

        for skill in self.skills.all().prefetch_related("skillstatus_set__employee"):
            for item in skill.skillstatus_set.all():
                if item.employee.firstname == self.firstname:
                    total += 1
                    if item.status:
                        completed += 1
        try:
            percentage = round((completed / total * 100), 2)
        except ZeroDivisionError:
            percentage = 0.0
        return f"{percentage} %"

class SkillStatus(models.Model):
    employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
    skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
    status = models.BooleanField(default=False)

Моя основная проблема связана с методом skills_percentage, я делаю слишком много запросов при вычислении указанного значения. Я уже немного улучшил ситуацию с prefetch_related, но в Django Debug Toolbar все еще есть лишние запросы. Что еще здесь можно сделать?

Я пробовал играть с различными комбинациями select_related и prefetch_related. Я думал о других вариантах для расчета skills_percentage, но они также требовались для многих запросов...

Заранее спасибо.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
56
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы можете попробовать так:

from django.db.models import Count, When, Case, Cast, FloatField

employees = Employee.objects.annotate(
        total=Count('skills',distinct=True),
        completed = Count('skills', filter=Q(skillstatus__status=True),distinct=True)
    ).annotate(
        percentage= Case(
            When(total=0, then=0.0),
            default=(Cast(
                F('completed')*100 / F('total'),
                output_field=FloatField()
            )
        )
    )
)

# usage

for employee in employees:
    print(employee.percentage)

# or 

employees.values('firstname', 'percentage')

Здесь я считаю навыки дважды, один раз без каких-либо условий для общего количества и второй раз с условием фильтра. Затем я аннотирую разделение завершенного/общего значения исходным набором запросов, преобразовывая его как FloatField.

Большое спасибо. Это немного уменьшило количество запросов, но «сотрудники» сами по себе являются набором запросов (а не числом). Или я что-то не так делаю?

Антон Ласько 17.02.2023 17:44

вы можете получить доступ к этим значениям через цикл или набор запросов значений. Я добавлю несколько примеров в ответ

ruddra 17.02.2023 17:47

Здравствуйте, большое спасибо за поправку. Это избавило от лишних запросов, но, к сожалению, не у всех моих сотрудников результат верный. Похоже процент рассчитывается неправильно (может быть 1 или 0). Проблема где-то в "завершенном" значении... Я уже изменил "filter=Q(skills__status=True)" на "filter=Q(skills__skillstatus__status=True)", но что-то еще есть

Антон Ласько 20.02.2023 09:55

Исправлено с небольшой модификацией)employees = Employee.objects.annotate( total=Count('skillstatus', Different=True), Completed=Count('skillstatus', filter=Q(skillstatus__status=True), Different=True), ) .annotate (процент = регистр (когда (всего = 0, затем = 0,0), по умолчанию = (приведение (F («завершено») * 100 / F («всего»), output_field = FloatField () ) )) )

Антон Ласько 20.02.2023 10:52

Вы можете использовать функции агрегации, предоставляемые Django ORM. Это может помочь сократить количество запросов, необходимых для подсчета количества выполненных и общих навыков для сотрудника.

Использование выражений F() и annotate() позволяет нам выполнять вычисления в одном запросе без необходимости отдельного цикла для связанных объектов Skill.

from django.db.models import Count, F

class Employee(models.Model):
    ...

    def skills_percentage(self):
        counts = self.skills.annotate(
            completed_count=Count('skillstatus', filter=Q(skillstatus__status=True, skillstatus__employee=self)),
            total_count=Count('skillstatus', filter=Q(skillstatus__employee=self)),
        ).aggregate(
            completed_count_sum=Sum('completed_count'),
            total_count_sum=Sum('total_count'),
        )

        completed = counts['completed_count_sum'] or 0
        total = counts['total_count_sum'] or 0
        try:
            percentage = round((completed / total * 100), 2)
        except ZeroDivisionError:
            percentage = 0.0
        return f"{percentage} %"

Это определенно улучшило ситуацию, спасибо большое :) Теперь осталось всего 6 похожих запросов (было 9 на 3 сотрудников). Значит, вообще не нужно использовать prefetch_related?

Антон Ласько 17.02.2023 13:00

Вы можете использовать prefetch_related, чтобы еще больше сократить количество запросов, если они связаны с доступом к объектам Skill, связанным с экземплярами Employee. prefetch_related можно использовать для извлечения всех связанных объектов в одном запросе, что может быть более эффективным, чем извлечение их по одному в цикле. Однако имейте в виду, что prefetch_related может быть дорогостоящим с точки зрения использования памяти, поскольку он одновременно извлекает все связанные объекты в памяти. Поэтому вы должны использовать его разумно и только тогда, когда это имеет смысл делать в зависимости от конкретного варианта использования.

Leli Michael 17.02.2023 14:03

И есть ли способ объединить расчет «завершено» и «всего» в 1 набор запросов? Я пытался сделать это с помощью объекта F, но это не мой первый большой проект Django :)

Антон Ласько 17.02.2023 14:55

Да, можно объединить вычисление «завершено» и «всего» в один набор запросов, используя выражения Django F().

Leli Michael 17.02.2023 19:30

Другие вопросы по теме