Използване на .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
Имам няколко идеи за отговор, но дали изобщо би било полезно и какъв ще бъде подходящият начин за конкретното му прилагане зависи от някои специфики на вашите модели и/или структура на DB, които изглежда глупаво да се опитвате да заключите, тъй като вие могат да предоставят това, което всъщност са (също и за теб, Трей). Бихте ли ги публикували (за предпочитане) или тяхна премахната/опростена версия, за да мога да пробвам?   -  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