Django: Как сделать переопределение полей на основе его значения в запросе агрегации (объединение в несколько столбцов) - PullRequest
1 голос
/ 30 января 2020

Я некоторое время искал по разным углам inte rnet и Django Проектной документации без успеха, поэтому вот мой вопрос:

В качестве фона я синхронизирую модели с внешним API, поэтому я бы предпочел не менять структуру модели.

У меня есть следующие модели (просто для краткости оставил соответствующие поля):

  • TimeEntry:

    class TimeEntry(models.Model):
         id = CharField(primary_key=True, max_length=255)
         duration_decimal = DecimalField(max_digits=5, decimal_places=2, verbose_name='dec duration')
         start = DateTimeField()
         project = ForeignKey(Project, models.SET_NULL, blank=True, null=True)
         user = ForeignKey(User, models.SET_NULL, blank=True, null=True)
    
  • Членство

    class Membership(models.Model):
        user = ForeignKey(User, models.SET_NULL, blank=True, null=True)
        project = ForeignKey(Project, models.SET_NULL, blank=True, null=True)
        hourly_rate = ForeignKey(HourlyRate, models.SET_NULL, blank=True, null=True)
        active = BooleanField(default=True)
    
  • Пользователь

    class User(models.Model):
        hourly_rate = DecimalField(max_digits=7, decimal_places=2, blank=True, null=True)
    
  • HourlyRate

    class HourlyRate(models.Model):
        amount = MoneyField(max_digits=10, decimal_places=2, blank=True, null=True, default_currency='USD')
    

Почасовая ставка может быть установлена ​​на уровне пользователя и может быть переопределена на уровне членства, поэтому я пытаюсь создать запрос Django -ORM, чтобы получить значение переопределенной почасовой ставки или пользовательской почасовой в случае, если значение на уровне членства не определено, умножьте его на десятичную продолжительность, а затем агрегируйте все за месяц, чтобы сгенерировать отчет о потребленных часах и его денежном значении.

Здесь У меня есть запрос, который в настоящее время работает для почасовой ставки на уровне пользователя:

  • Запрос уровня пользователя

    TimeEntry.objects.filter(project_id='xxxxxx')
    .annotate(year=ExtractYear('start'), month=ExtractMonth('start'))\
    .values('year', 'month')\
    .annotate(sum=ExpressionWrapper(Sum(F('user__hourly_rate')*F('duration_decimal')), DecimalField()), hours_sum=Sum('duration_decimal'), count=Count('id'))\
    .values('year', 'month', 'count', 'sum').order_by('-year', '-month')
    
  • Вывод:

    +----------------------------------------------+
    |                    Results                   |
    +----+------+-------+------+-----------+-------+
    | No | year | month | sum  | hours_sum | count |
    +----+------+-------+------+-----------+-------+
    | 1  | 2020 |     1 | 1000 |        95 |    15 |
    +----+------+-------+------+-----------+-------+
    | 2  | 2019 |    12 |  990 |        87 |     9 |
    +----+------+-------+------+-----------+-------+
    | 3  | 2019 |    11 | 1250 |       113 |    21 |
    +----+------+-------+------+-----------+-------+
    |    | ...  |       |      |           |       |
    +----+------+-------+------+-----------+-------+
    

Это моя попытка получить переопределенные значения из модели членства

  • Запрос уровня членства
    TimeEntry.objects.filter(project_id='xxxxxx')
    .annotate(year=ExtractYear('start'), month=ExtractMonth('start'))\
    .values('year', 'month')\
    .annotate(sum=ExpressionWrapper(Sum(F('project__membership__hourly_rate__amount')*F('duration_decimal')), DecimalField()), count=Count('id'))\
    .order_by('-year', '-month').values('year', 'month', 'count', 'sum')
    

И вот сгенерированный SQL для этого запроса:

  • Уровень участия генерирует SQL
    SELECT EXTRACT('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich')  AS "year",
      EXTRACT('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich') AS "month",
      SUM(("hourlyrate"."amount" * "timeentry"."duration_decimal")) AS "sum",
      COUNT("timeentry"."id")                                                AS "count"
    FROM "timeentry"
        INNER JOIN "project" ON ("timeentry"."project_id" = "project"."id")
        LEFT OUTER JOIN "membership" ON ("project"."id" = "clockify_membership"."project_id")
        LEFT OUTER JOIN "hourlyrate" ON ("membership"."hourly_rate_id" = "hourlyrate"."id")
    WHERE "timeentry"."project_id" = 'xxxxxx'
    GROUP BY EXTRACT ('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich'), EXTRACT ('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich')
    ORDER BY "year" DESC, "month" DESC
    

Проблема с сгенерированным SQL заключается в том, что первые два соединения объединены и не объединены в одну (что приводит к слишком большому количеству строк), вот SQL, который дает правильный результат:

  • Уровень членства генерирует SQL [исправлено]
    SELECT EXTRACT('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich')  AS "year",
      EXTRACT('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich') AS "month",
      SUM(("hourlyrate"."amount" * "timeentry"."duration_decimal")) AS "sum",
      COUNT("timeentry"."id")                                                AS "count"
    FROM "timeentry"
        -- START> Here is the JOIN over two columns consolidating the two previous separated JOINs
        LEFT OUTER JOIN "membership" ON ("timeentry"."user_id" = "membership"."user_id" AND "membership"."project_id" = "timeentry"."project_id")
        -- <END
        LEFT OUTER JOIN "hourlyrate" ON ("membership"."hourly_rate_id" = "hourlyrate"."id")
    WHERE "timeentry"."project_id" = 'xxxxxx'
    GROUP BY EXTRACT ('year' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich'), EXTRACT ('month' FROM "timeentry"."start" AT TIME ZONE 'Europe/Zurich')
    ORDER BY "year" DESC, "month" DESC
    

Я думаю, что могу придумать альтернативное решение, используя SubQuery, но тогда я не знаю, как обработать переопределение в этом сценарии.

1 Ответ

0 голосов
/ 03 февраля 2020

Я предлагаю вам определить ForeignObject для вашей TimeEntry модели, которая указывает на Membership.

from django.db.models.fields.related import ForeignObject


class TimeEntry(models.Model):
    ...
    project = ForeignKey(Project, models.SET_NULL, blank=True, null=True)
    user = ForeignKey(User, models.SET_NULL, blank=True, null=True)
    membership = ForeignObject(
        Membership,
        from_fields=('project', 'user'), 
        to_fields=('project', 'user')
    )

С этим определенным отношением ваш набор запросов сможет использовать Coalesce('membership__hourly_rate', 'membership__user__hourly_rate').

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...