Использование значения аннотации в последующих аннотациях создает FieldError - PullRequest
1 голос
/ 03 апреля 2020

Что я пытаюсь сделать:

У меня есть модели Topic и Entry. Entry имеет ForeignKey to topi c. Мне нужно перечислить темы при условии, что в нем есть записи пользователя (созданные за последние 24 часа). Мне также нужно аннотировать количество, это должно быть общее количество записей, созданных после последней записи, написанной пользователем. (Короче говоря, вы можете вспомнить папку «Входящие», в которой у вас есть список разговоров с количеством непрочитанных сообщений.)

Вот что я придумал:

relevant_topics = (
    Entry.objects.filter(author=user, date_created__gte=time_threshold(hours=24))
    .values_list("topic__pk", flat=True)
    .order_by()
    .distinct()
)

qs = (
    Topic.objects.filter(pk__in=relevant_topics).annotate(
        latest=Max("entries__date_created", filter=Q(entries__author=user)),
        count=Count("entries", filter=Q(date_created__gte=F("latest__date_created"))),
    )
).values("title", "count")

Который выбросит:

FieldError: Cannot resolve keyword 'date_created' into field. Join on 'latest' not permitted.

Я не знаю, действительно ли Django не поддерживает то, что я написал, или мое решение неверно. Я думал добавить счетчик, используя .extra (), но я не мог понять, как использовать аннотацию latest там. Я был бы очень признателен за любой запрос, который выдает ожидаемый результат.

Набор справочных данных:

(assume the current user = Jack)

<User username: Jack>
<User username: John>

<Topic title: foo>
<Topic title: bar>
<Topic title: baz>

(Assume higher pk = created later.)

<Entry pk:1 topic:foo user:Jack>
<Entry pk:2 topic:foo user:Jack> (date_created in last 24 hours)
<Entry pk:3 topic:foo user:John> (date_created in last 24 hours)

<Entry pk:4 topic:bar user:Jack> (date_created in last 24 hours)

<Entry pk:5 topic:baz user:John> (date_created in last 24 hours)

Given the dataset, the output should only be:

<Topic:foo count:1>

РЕДАКТИРОВАТЬ:

Чтобы дать вам представление, вот сырье SQL решение, которое дает правильный вывод:

    pk = user.pk
    threshold = time_threshold(hours=24)

    with connection.cursor() as cursor:
        cursor.execute(
            """
        select
          s.title,
          s.slug,
          s.count
        from
          (
            select
              tt.title,
              tt.slug,
              e.count,
              e.max_id
            from
              (
                select
                  z.topic_id,
                  count(
                    case when z.id > k.max_id then z.id end
                  ) as count,
                  k.max_id
                from
                  dictionary_entry z
                  inner join (
                    select
                      topic_id,
                      max(de.id) as max_id
                    from
                      dictionary_entry de
                    where
                      de.date_created >= %s
                      and de.author_id = %s
                    group by
                      author_id,
                      topic_id
                  ) k on k.topic_id = z.topic_id
                group by
                  z.topic_id,
                  k.max_id
              ) e
              inner join dictionary_topic tt on tt.id = e.topic_id
          ) s
        where
          s.count > 0
        order by
          s.max_id desc
        """,
            [threshold, pk],
        )
        # convert to dict
        columns = [col[0] for col in cursor.description]
        return [dict(zip(columns, row)) for row in cursor.fetchall()]

Ответы [ 2 ]

1 голос
/ 09 апреля 2020

Этого можно достичь в запросе 1 SQL в базе данных путем

  1. фильтрации соответствующей entries (важным битом является OuterRef, который "передает" фильтр в topics),
  2. , сгруппировав entries по topic и используя count, а затем
  3. , аннотируя topics, используя Subquery.

Информацию об этом можно найти в Django документах .

. В вашем случае следующее должно дать желаемый результат.

from django.db.models import Count, IntegerField, OuterRef, Subquery

relevant_topics = (
    models.Entry.objects.filter(
        author=user, date_created__gte=time_threshold(24), topic=OuterRef("pk"),
    )
    .order_by()
    .values("topic")
    .annotate(Count("id"))
    .values("id__count")
)

qs = models.Topic.objects.annotate(
    entries_count=Subquery(relevant_topics, output_field=IntegerField())
).filter(entries_count__gt=0)

Надеюсь, это поможет : -)

Редактировать 1:

Я думаю, что я неправильно понял вопрос и забыл принять во внимание тот факт, что это entries других авторов, которые нужно посчитать (после последнего из текущего автора).

Итак, я придумал следующее, что дает те же результаты, что и ответ @Paul Rene :

latest_in_topic = (
    Entry.objects.filter(author=user, date_created__gte=time_threshold(24), topic=OuterRef("topic"))
    .values("topic")
    .annotate(latest=Max("date_created"))
)

qs = (
    Entry.objects.annotate(
        latest=Subquery(latest_in_topic.values("latest"), output_field=DateTimeField())
    )
    .filter(date_created__gte=F("latest"))
    .values("topic", "topic__title")
    .annotate(Count("id"))
)

res = [(t["topic__title"], t["id__count"]) for t in qs]

Редактировать 2: ORM создает следующий запрос (полученный из str(qs.query)). Возможно, будет какая-то подсказка, как улучшить производительность.

SELECT "entry"."topic_id", "topic"."title", COUNT("entry"."id") AS "id__count"
FROM "entry"
         INNER JOIN "topic" ON ("entry"."topic_id" = "topic"."id")
WHERE "entry"."date_created" > (SELECT MAX(U0."date_created") AS "latest"
                                    FROM "entry" U0
                                    WHERE (U0."author_id" = 1 AND U0."date_created" >= '2020-04-09 16:31:48.407501+00:00' AND U0."topic_id" = ("entry"."topic_id"))
                                    GROUP BY U0."topic_id")
GROUP BY "entry"."topic_id", "topic"."title";
1 голос
/ 07 апреля 2020

Я перестроил ваш запрос, надеюсь, я правильно понял вашу цель. Я пришел с той же ошибкой. Кажется, это как-то связано с тем, как SQL оценивает запросы. Я перефразирую ваши запросы следующим образом:

    qs0 = Topic.objects.filter(
        entries__author=user, entries__date_created__gte=time_threshold(24)).annotate(
            latest=Max("entries__date_created")
        )
    qs1 = qs0.annotate(
        count=Count("entries", filter=Q(entries__date_created__gte=F("latest")))
        ).values("title", "count")

Поэтому я сначала отфильтрую последние темы, в которых «пользователь» имел записи, и аннотирую их датой последней записи (qs0), а затем попытаюсь аннотировать этот запрос с желаемым количеством. Первый запрос делает то, что должен; когда я распечатываю или оцениваю его в виде списка, результаты мне кажутся правильными (я использовал фиктивные данные). Но со вторым запросом я получаю следующее сообщение об ошибке:

aggregate functions are not allowed in FILTER
LINE 1: ...") FILTER (WHERE "dummy_entry"."date_created" >= (MAX("dummy...

Копание по inte rnet говорит мне, что это может быть связано с тем, как SQL обрабатывает WHERE. Я пробовал оба MySQL и PostgreSQL, оба выдавали ошибки. На мой взгляд, второй запрос синтаксически корректен, но поскольку первый запрос не оценивается до его подачи во второй, именно так и происходит ошибка.

В любом случае, я смог получить желаемый результат (снова (если я вас правильно понимаю), хотя и очень уродливо, используя следующий код вместо второго запроса:

    dict = {}
    for item in qs0:
        dict[item.pk] = [item.title, item.latest, 0]

    for entry in Entry.objects.all():
        if entry.date_created >= dict[entry.topic.pk][1]:
            dict[entry.topic.pk][2] += 1

Я поставил qs0 в диктовку с ключом pk и сделал подсчитайте все записи вручную.

Боюсь, это лучшее, что я могу сделать. Я искренне надеюсь, что кто-то придумает более элегантное решение!

РЕДАКТИРОВАТЬ после прочтения ответа Krysotl:

Не окончательный ответ, но, возможно, это поможет. В большинстве случаев WHERE нельзя использовать перед агрегатными функциями, см. Агрегатная функция в SQL WHERE-Clause . Иногда это можно исправить, заменив WHERE на HAVING в SQL. Django может обрабатывать необработанные SQL запросы, см. https://docs.djangoproject.com/en/3.0/ref/models/expressions/#raw - sql -выражения . Поэтому я попробовал следующее:

sql_command = '''SELECT entry.topic_id, topic.title, entry.date_created, COUNT(entry.id) AS id__count FROM entry
        INNER JOIN topic ON (entry.topic_id = topic.id) GROUP BY entry.topic_id, topic.title, entry.date_created
        HAVING entry.date_created > (SELECT MAX(U0.date_created) AS latest
        FROM entry U0 WHERE (U0.author_id = 1 AND U0.date_created >= '2020-04-09 16:31:48.407501+00:00'
        AND U0.topic_id = (entry.topic_id)) GROUP BY U0.topic_id)'''

    qs = Entry.objects.annotate(val=RawSQL(sql_command, ()))

Другими словами: поместите GROUP BY перед WHERE и замените WHERE на HAVING. К сожалению, это все еще дало мне ошибки. Боюсь, мне недостаточно эксперта SQL, чтобы решить эту проблему, но, возможно, это путь вперед.

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