Использование .aggregate() для значения, введенного с помощью .extra(select={}) в запросе Django?

Я пытаюсь подсчитать, сколько раз игрок играл каждую неделю следующим образом:

player.game_objects.extra(
    select={'week': 'WEEK(`games_game`.`date`)'}
).aggregate(count=Count('week'))

Но Джанго жалуется, что

FieldError: Cannot resolve keyword 'week' into field. Choices are: <lists model fields>

Я могу сделать это в чистом SQL, как это

SELECT WEEK(date) as week, COUNT(WEEK(date)) as count FROM games_game
WHERE player_id = 3
GROUP BY week

Есть ли хороший способ сделать это без выполнения необработанного SQL в Django?


person Jake    schedule 31.12.2010    source источник
comment
Вы должны показать свои модели, наверное. Работает ли QS без агрегации?   -  person lprsd    schedule 31.12.2010
comment
Да, player.game_objects.extra(select={'week': 'WEEK(games_game.date)'})[0].week дает 43L, как и ожидалось.   -  person Jake    schedule 05.01.2011
comment
Мои модели довольно сложны, это упрощение моей проблемы. Если это поможет, я мог бы написать тестовый пример с простыми моделями.   -  person Jake    schedule 05.01.2011
comment
У меня есть пара идей для ответа, но будет ли он даже полезен, и какой подходящий способ его конкретной реализации будет зависеть от некоторых особенностей ваших моделей и/или структуры БД, которые кажется глупо пытаться вывести, поскольку вы могут предоставить то, чем они являются на самом деле (то же самое для вас, Трей). Не могли бы вы опубликовать их (предпочтительно) или их опущенную/упрощенную версию, чтобы я мог попробовать?   -  person desfido    schedule 03.03.2011
comment
Я только что опубликовал ответ, содержащий пример сценария, в котором это проблема, и обходное решение, которое лучше, чем использование необработанного SQL, но все же неидеально.   -  person Trey Hunner    schedule 03.03.2011


Ответы (2)


Вы можете использовать пользовательскую агрегатную функцию для создания вашего запроса:

WEEK_FUNC = 'STRFTIME("%%%%W", %s)' # use 'WEEK(%s)' for mysql

class WeekCountAggregate(models.sql.aggregates.Aggregate):
    is_ordinal = True
    sql_function = 'WEEK' # unused
    sql_template = "COUNT(%s)" % (WEEK_FUNC.replace('%%', '%%%%') % '%(field)s')

class WeekCount(models.aggregates.Aggregate):
    name = 'Week'
    def add_to_query(self, query, alias, col, source, is_summary):
        query.aggregates[alias] = WeekCountAggregate(col, source=source, 
            is_summary=is_summary, **self.extra)


>>> game_objects.extra(select={'week': WEEK_FUNC % '"games_game"."date"'}).values('week').annotate(count=WeekCount('pk'))

Но поскольку этот API недокументирован и уже требует битов необработанного SQL, вам может быть лучше использовать необработанный запрос .

person emulbreh    schedule 04.03.2011
comment
О, мне нравится, как это звучит. - person Jake; 06.03.2011
comment
Используйте YEARWEEK вместо WEEK, если ваш диапазон охватывает несколько лет: - person Xerion; 31.12.2014

Вот пример проблемы и неидеальное обходное решение. Возьмите эту модель примера:

class Rating(models.Model):
    RATING_CHOICES = (
        (1, '1'),
        (2, '2'),
        (3, '3'),
        (4, '4'),
        (5, '5'),
    )
    rating = models.PositiveIntegerField(choices=RATING_CHOICES)
    rater = models.ForeignKey('User', related_name='ratings_given')
    ratee = models.ForeignKey('User', related_name='ratings_received')

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

User.ratings_received.extra(
    select={'percent_positive': 'ratings > 3'}
).aggregate(count=Avg('positive'))

Единое обходное решение

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

User.ratings.extra(
    select={'percent_positive': 'AVG(rating >= 3)'}
)

Этот запрос сгенерирует следующий SQL-запрос:

SELECT (AVG(rating >= 3)) AS `percent_positive`,
       `ratings_rating`.`id`,
       `ratings_rating`.`rating`,
       `ratings_rating`.`rater_id`,
       `ratings_rating`.`ratee_id`
FROM `ratings_rating`
WHERE `ratings_rating`.`ratee_id` = 1

Несмотря на ненужные столбцы в этом запросе, мы все равно можем получить из него нужное значение, выделив значение percent_positive:

User.ratings.extra(
    select={'percent_positive': 'AVG(rating >= 3)'}
).values('percent_positive')[0]['percent_positive']
person Trey Hunner    schedule 03.03.2011
comment
Этот обходной путь - именно то, что я сделал, но будьте осторожны, чтобы получить пустой возврат (т.е. User.ratings пуст), так как он выдаст IndexError - person jelford; 30.08.2011
comment
@jelford: я думаю, что в этом случае он просто возвращает None, поскольку, по крайней мере, в SQLite AVG() пустого набора возвращает NULL. - person Mechanical snail; 26.10.2013