14

Je construis une base de données de journalisation des aliments dans Django et j'ai un problème lié à la requête.Comment trier par annotated Count() dans un modèle associé dans Django

J'ai configuré mes modèles pour inclure (entre autres) un modèle Food connecté au modèle User via un «consommateur» de champ M2M via le modèle Consommation. Le modèle Food décrit les plats d'aliments et le modèle de consommation décrit la consommation d'aliments par un utilisateur (date, quantité, etc.).

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") 

Je veux créer une requête qui retourne tous les objets alimentaires commandés par le nombre de fois que l'objet alimentaire apparaît dans la table de consommation pour cet utilisateur (le nombre de fois que l'utilisateur a consommé la nourriture).

J'essaie quelque chose dans la ligne de:

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

Mais ce sera bien sûr compter tous les objets de consommation liés à l'objet alimentaire, pas seulement ceux associés à l'utilisateur. Ai-je besoin de changer de modèle ou est-ce que je manque quelque chose d'évident dans les requêtes?

Il s'agit d'une opération assez complexe (entre autres, elle est utilisée pour remplir un champ de saisie semi-automatique dans le Frontend) et la table Food a quelques milliers d'entrées, donc je préfère trier dans la base de données fin de compte, plutôt que de faire la méthode de la force brute et itérer sur les résultats faisant:

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

puis trier en utilisant python pour les trier. Je ne pense pas que cette méthode évolue très bien au fur et à mesure que la base d'utilisateurs augmente et je veux concevoir la base de données comme une preuve d'avenir dès que possible.

Des idées?

+0

double possible de [Ordre par comptage d'un champ ForeignKey?] (Http://stackoverflow.com/questions/2501149/order-by-count-of-a-foreignkey-field) –

Répondre

21

Peut-être que quelque chose comme ça?

Food.objects.filter(consumer__user=user)\ 
      .annotate(consumption_times=Count('consumer'))\ 
      .order_by('consumption_times') 
+0

Mais cela ne retourner que les objets alimentaires qui ont été consommés à un moment donné, n'est-ce pas? Je veux retourner tous les objets alimentaires, mais dans l'ordre du plus souvent consommés en premier. Si je filtre par utilisateur, je n'obtiendrai pas la nourriture qui n'a pas encore été consommée. Une idée serait peut-être de faire deux requêtes, la première comme vous avez suggéré d'obtenir tous les éléments de nourriture consommés au moins une fois et ensuite quelque chose dans le genre de Food.objects.exclude (consumer__user = user) et remplir la liste avec ceux . Cela fonctionnerait-il? –

+0

Oui, 2 questions seraient comment je le ferais. – SmileyChris

19

J'ai un problème très similaire. Au fond, je sais que la requête SQL que vous voulez est:

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; 

Ce que je souhaite est que vous pouvez mélanger des fonctions d'agrégation et d'expression F, annoter expressions F sans fonction d'agrégation, un ensemble plus riche d'opérations/fonctions pour F expressions, et ont des champs virtuels qui sont essentiellement une annotation d'expression F automatique. Alors que vous pourriez faire:

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

Aussi, tout pouvoir plus facilement en mesure d'ajouter vos propres fonctions d'agrégation complexes serait bien, mais en attendant, voici un hack qui ajoute une fonction d'agrégation pour le faire.

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') 
+0

C'est génial !!! Merci l'homme, vous avez sauvé ma journée!Je vais essayer de le rendre un peu plus joli, mais vous devriez certainement le déposer dans le trac de django. –

Questions connexes