如何在Django中的相关模型中按注释的Count()进行排序

Jen*_*Alm 15 django aggregate-functions django-models django-queryset

我正在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")
Run Code Online (Sandbox Code Playgroud)

我想创建一个查询,返回按Food对象出现在该用户的消耗表中的次数排序的所有Food对象(用户食用食物的次数).

我正在尝试以下方面:

Food.objects.all().annotate(consumption_times = Count(consumer)).order_by('consumption_times')`
Run Code Online (Sandbox Code Playgroud)

但这当然会计算与Food对象相关的所有Consumer对象,而不仅仅是与用户关联的对象.我是否需要更改模型,或者我只是遗漏了查询中明显的内容?

这是一个非常关键的时间操作(除此之外,它用于填充前端的自动完成字段)和Food表有几千个条目,所以我宁愿在数据库端进行排序,而不是做蛮力方法并迭代结果做:

Consumption.objects.filter(food=food, user=user).count()
Run Code Online (Sandbox Code Playgroud)

然后使用python排序对它们进行排序.我不认为随着用户群的增加,这种方法会扩展得很好,我想从头开始设计数据库作为未来的证据.

有任何想法吗?

Smi*_*ris 24

也许是这样的?

Food.objects.filter(consumer__user=user)\
            .annotate(consumption_times=Count('consumer'))\
            .order_by('consumption_times')
Run Code Online (Sandbox Code Playgroud)


spa*_*kyb 20

我有一个非常类似的问题.基本上,我知道你想要的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;
Run Code Online (Sandbox Code Playgroud)

我希望你可以混合聚合函数和F表达式,注释没有聚合函数的F表达式,为F表达式提供更丰富的操作/函数,并且具有基本上是自动F表达式注释的虚拟字段.这样你就可以做到:

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

此外,只是能够更容易地添加自己的复杂聚合函数会很好,但与此同时,这是一个增加聚合函数来执行此操作的hack.

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')
Run Code Online (Sandbox Code Playgroud)