Как сортировать по аннотированному Count() в связанной модели в Django

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

Я настроил свои модели так, чтобы они включали (среди прочего) модель еды, связанную с моделью пользователя через M2M-поле «потребитель» через модель потребления. Модель еды описывает блюда из еды, а модель потребления описывает потребление еды пользователем (дата, количество и т. д.).

class Food(models.Model):
    food_name = models.CharField(max_length=30)
    consumer = models.ManyToManyField("User", through=Consumption)

class Consumption(models.Model):
    food = models.ForeignKey("Food")
    user = models.ForeignKey("User")

Я хочу создать запрос, который возвращает все объекты Food, упорядоченные по количеству раз, когда объект Food появляется в таблице потребления для этого пользователя (количество раз, когда пользователь потреблял пищу).

Я пытаюсь что-то в строке:

Food.objects.all().annotate(consumption_times = Count(consumer)).order_by('consumption_times')`

Но это, конечно, будет учитывать все объекты потребления, связанные с объектом Food, а не только те, которые связаны с пользователем. Нужно ли мне менять свои модели или я просто упускаю что-то очевидное в запросах?

Это довольно критичная по времени операция (среди прочего, она используется для заполнения поля автозаполнения во внешнем интерфейсе), а в таблице Food есть пара тысяч записей, поэтому я бы предпочел выполнять сортировку в конце базы данных, а не выполняя метод грубой силы и перебирая результаты, выполняя:

Consumption.objects.filter(food=food, user=user).count()

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

Любые идеи?


person Jens Alm    schedule 08.09.2009    source источник
comment
Возможный дубликат Порядок по количеству поля ForeignKey?   -  person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 20.05.2016


Ответы (2)


Возможно, что-то вроде этого?

Food.objects.filter(consumer__user=user)\
            .annotate(consumption_times=Count('consumer'))\
            .order_by('consumption_times')
person SmileyChris    schedule 08.09.2009
comment
Но это вернет только те объекты Food, которые когда-то были потреблены, не так ли? Я хочу вернуть все объекты Food, но в том порядке, в котором они чаще всего употребляются в первую очередь. Если я отфильтрую по пользователю, я не получу Еду, которая еще не съедена. Одна из идей, возможно, состоит в том, чтобы сделать два запроса: первый, как вы предложили, чтобы получить все продукты питания, потребляемые хотя бы один раз, а затем что-то вроде строк Food.objects.exclude(consumer__user = user) и заполнить список этими . Будет ли это работать? - person Jens Alm; 09.09.2009
comment
Да, 2 вопроса, как бы я это сделал. - person SmileyChris; 10.09.2009

У меня очень похожая проблема. По сути, я знаю, что вам нужен SQL-запрос:

SELECT food.*, COUNT(IF(consumption.user_id=123,TRUE,NULL)) AS consumption_times
       FROM food LEFT JOIN consumption ON (food.id=consumption.food_id)
       ORDER BY consumption_times;

Я бы хотел, чтобы вы могли смешивать агрегатные функции и F-выражение, аннотировать F-выражения без агрегатной функции, иметь более богатый набор операций/функций для F-выражений и иметь виртуальные поля, которые в основном являются автоматической аннотацией F-выражения. Чтобы вы могли сделать:

Food.objects.annotate(consumption_times=Count(If(F('consumer')==user,True,None)))\
            .order_by('consumtion_times')

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

from django.db.models import aggregates,sql
class CountIf(sql.aggregates.Count):
    sql_template = '%(function)s(IF(%(field)s=%(equals)s,TRUE,NULL))'
sql.aggregates.CountIf = CountIf

consumption_times = aggregates.Count('consumer',equals=user.id)
consumption_times.name = 'CountIf'
rows = Food.objects.annotate(consumption_times=consumption_times)\
                   .order_by('consumption_times')
person sparkyb    schedule 25.11.2009
comment
Это круто!!! Спасибо, чувак, ты спас мой день! Я постараюсь, чтобы это выглядело немного красивее, но вы обязательно должны записать это в trac django. - person Grégoire Cachet; 26.11.2009