Максимальное значение Django из столбцов объединенной таблицы

Я пытаюсь получить максимальное значение от объединения результатов из двух отдельных таблиц. Я пытаюсь получить самый последний комментарий к сообщению. У меня есть модель Comment, к которой может получить доступ Post.comments. У меня также есть тело, отдельное от Post, доступ к которому может получить Post.body. И комментарии, и тело имеют поле when, которое является DateTimeField. Я хотел бы вернуть отсортированный набор запросов по самой последней активности, чтобы сначала отображалась запись с самым последним комментарием или телом.

Предположим, что модели выглядят так:

class Body(models.Model):
    when = models.DateTimeField(default=datetime.datetime.now)

class Post(models.Model):
    body = models.ForeignKey(Body)

class Comment(models.Model):
    post = models.ForeignKey(Post, related_name='comments')
    when = models.DateTimeField(default=datetime.datetime.now)

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

В принципе, я хотел бы иметь возможность звонить:

q = Post.annotate(
    activity=Max(
        Union('comments__when', 'body__when')
    )
)
q = q.order_by('-activity')

Но я не знаю, как осуществить этот Союз.

Я считаю, что SQL, который выполняет то, что я ищу, сравним с:

SELECT
...
IF(MAX(c.`when`), MAX(c.`when`), b.`when`) AS `activity`
...
FROM `post` p
...
LEFT OUTER JOIN `comment` AS c
ON c.`post_id`=p.`id`
LEFT OUTER JOIN `body` AS b
ON p.`body_id`=b.`id`
...

Можно ли выполнить такую ​​индивидуальную аннотацию и соединение?


person garromark    schedule 29.05.2012    source источник


Ответы (1)


Мне потребовалось очень много времени, чтобы понять это. Одна большая проблема заключается в том, что Django не поддерживает несколько условий для предложения ON оператора JOIN. Из-за зависимости одного оператора SELECT от двух отдельных операторов JOINs возникает проблема с точным отслеживанием имен таблиц. Когда вы просите django аннотировать таблицу, скажем, с помощью Max(), вы получаете такие условия, как Max(T7.when) ... LEFT OUT JOIN table AS T7, где T7 несовместим. Таким образом, мне нужен был способ точно сгенерировать выражение Max(T7.when) с учетом JOINs, автоматически сгенерированного Django. Многие сообщения в Интернете советуют вам использовать поддержку .raw() для наборов запросов, но в этом случае вы потеряете много преимуществ от использования ORM.

Решение, которое я придумал, заключалось в создании пользовательской агрегатной функции. Я назвал эту функцию CustomMax():

from django.conf import settings
from django.db import models

sum_if_sql_template = 'GREATEST(%(alt_table)s.%(alt_field)s, IFNULL(%(function)s(%(field)s), %(alt_table)s.%(alt_field)s))'

class CustomMaxSQL(models.sql.aggregates.Aggregate):
    sql_function = 'MAX'
    sql_template = sum_if_sql_template


class CustomMax(models.aggregates.Aggregate):
    name = 'Max'

    def __init__(self, lookup, **extra):
        self.lookup = lookup
        self.extra = extra

    def add_to_query(self, query, alias, col, source, is_summary):
        aggregate = CustomMaxSQL(col,
                             source=source,
                             is_summary=is_summary,
                             **self.extra)
        query.aggregates[alias] = aggregate

Использование функции:

q = Post.annotate(
    activity=CustomMax(
        'comments__when',
        alt_table="app_post",
        alt_field="when",
    )
)
q.query.join((
    'app_post',
    'app_comment',
    'id',
    'post_id',
))
q = q.order_by('-activity')

Я включаю .join(), чтобы позволить alt_table существовать как JOIN, а Django автоматически обработает имена операторов SELECT и JOIN для части Max. Сгенерированный SQL из этого использования похож на:

SELECT 
...
GREATEST(app_post.when, IFNULL(MAX(T7.`when`), app_post.when)) AS `activity`,
...
`app_post`.`id`
...
INNER JOIN `app_post` ON ...
...
LEFT OUTER JOIN `app_comment` T7 ON (`app_post`.`id` = T7.`post_id`)
...

Примечание. Это относится только к моделям Post и Comment, указанным выше. Моя фактическая реализация была немного сложнее и требовала этого решения.

Это также было бы намного проще, если бы команда Django разработала предложенный патч, чтобы включить операторы join в .extra(): https://code.djangoproject.com/ticket/7231

person garromark    schedule 30.05.2012