2 votes

Problème d'enchaînement d'objets Q() avec le même argument dans l'ORM de Django

Je travaille à la création d'une application de recettes de cocktails comme exercice d'apprentissage.

J'essaie de créer un filtre via le cadre Rest de Django qui accepte une chaîne d'identifiants d'ingrédients via un paramètre de requête (?=ingredients_exclusive=1,3,4), puis recherche toutes les recettes qui contiennent tous ces ingrédients. Je voudrais rechercher "Tous les cocktails qui ont à la fois du rhum et de la grenadine" et ensuite aussi, séparément "Tous les cocktails qui ont du rhum, et tous les cocktails qui ont de la grenadine".

Les trois modèles de mon application sont Recipes, RecipeIngredients et IngredientTypes. Les recettes (Old Fashioned) ont plusieurs RecipeIngredients (2oz de Whiskey), et les RecipeIngredients sont tous d'un IngredientTypes (Whiskey). Je vais éventuellement changer le RecipeIngredient en un modèle de passage en fonction de ce que je décide de faire.

La liste peut être d'une longueur variable, je ne peux donc pas simplement enchaîner les fonctions de filtrage. Je dois parcourir la liste des identifiants en boucle, puis construire un Q().

Cependant, j'ai quelques problèmes. Par le biais du shell Django, j'ai fait ceci :

>>> x = Recipe.objects.all()
>>> q = Q(ingredients__ingredient_type=3) & Q(ingredients__ingredient_type=7)
>>> x.filter(q)
<QuerySet []>
>>> x.filter(ingredients__ingredient_type=3).filter(ingredients__ingredient_type=7)
<QuerySet [<Recipe: Rum and Tonic>]>

Voici donc ma question : Pourquoi l'objet Q qui AND les deux requêtes est-il différent des filtres enchaînés du même objet ?

J'ai lu l'article " Recherches complexes avec des objets Q "dans la documentation de Django et cela ne semble pas aider.

Juste pour référence, voici mes filtres dans Filters.py.

La version "OR" de cette commande fonctionne correctement :

class RecipeFilterSet(FilterSet):
    ingredients_inclusive = django_filters.CharFilter(method='filter_by_ingredients_inclusive')
    ingredients_exclusive = django_filters.CharFilter(method='filter_by_ingredients_exclusive')

    def filter_by_ingredients_inclusive(self, queryset, name, value):
        ingredients = value.split(',')
        q_object = Q()
        for ingredient in ingredients:
            q_object |= Q(ingredients__ingredient_type=ingredient)
        return queryset.filter(q_object).distinct()

    def filter_by_ingredients_exclusive(self, queryset, name, value):
        ingredients = value.split(',')
        q_object = Q()
        for ingredient in ingredients:
            q_object &= Q(ingredients__ingredient_type=ingredient)
        return queryset.filter(q_object).distinct()

    class Meta:
        model = Recipe
        fields = ()

J'ai également inclus mes modèles ci-dessous :

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models

class IngredientType(models.Model):
  name = models.CharField(max_length=256)

  CATEGORY_CHOICES = (
    ('LIQUOR', 'Liquor'),
    ('SYRUP', 'Syrup'),
    ('MIXER', 'Mixer'),
  )

  category = models.CharField(
    max_length=128, choices=CATEGORY_CHOICES, default='MIXER')

  def __str__(self):
    return self.name

class Recipe(models.Model):
  name = models.CharField(max_length=256)

  def __str__(self):
    return self.name

class RecipeIngredient(models.Model):
  ingredient_type = models.ForeignKey(IngredientType, on_delete=models.CASCADE, related_name="ingredients")
  quantity = models.IntegerField(default=0)
  quantity_type = models.CharField(max_length=256)
  recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name="ingredients")

  @property
  def ingredient_type_name(self):
    return self.ingredient_type.name

  @property
  def ingredient_type_category(self):
    return self.ingredient_type.category

  def __str__(self):
    return f'{self.quantity}{self.quantity_type} of {self.ingredient_type}'

Toute aide serait très appréciée !

1voto

La différence entre les deux approches de filter() est décrit dans Étendue des relations multivaluées :

Tout ce qui se trouve dans un seul filter() est appliqué simultanément pour filtrer les éléments qui correspondent à toutes ces exigences..... Pour les relations à valeurs multiples, elles s'appliquent à tout objet lié au modèle primaire, et pas nécessairement aux objets qui ont été sélectionnés par un appel antérieur à l'adresse filter() appeler.

L'exemple dans la documentation rend les choses plus claires. Je vais le réécrire en fonction de votre problème :

Pour sélectionner toutes les recettes qui contiennent un ingrédient ayant à la fois le type 3 et le type 7 nous écririons :

Recipe.objects.filter(ingredients__ingredient_type=3, ingredients__ingredient_type=7)

C'est bien sûr impossible dans votre modèle, donc cela renverrait un queryset vide, tout comme votre Q exemple avec AND .

Pour sélectionner toutes les recettes qui contiennent un ingrédient de type 3 ainsi que un ingrédient de type 7 nous écririons :

Recipe.objects.filter(ingredients__ingredient_type=3).filter(ingredients__ingredient_type=7)

Ce n'est pas particulièrement intuitif, mais ils avaient besoin d'un moyen de distinguer ces deux cas et c'est ce qu'ils ont trouvé.


Pour en revenir à votre problème, le OR peut être simplifié en utilisant l'option in opérateur :

Recipe.objects.filter(ingredients__ingredient_type__in=[3, 7]).distinct()

El AND est compliqué car il s'agit d'une condition qui implique plusieurs lignes. Une approche simple consisterait à prendre le OR ci-dessus et le traiter ensuite en Python pour trouver le sous-ensemble qui a tous les ingrédients.

Une approche d'interrogation qui devrait fonctionner implique l'annotation à l'aide de Count . Ce n'est pas testé, mais quelque chose comme :

Recipe.objects.annotate(num_ingredients=Count("ingredients", 
                            filter=Q(ingredients__ingredient_type__in=[3, 7]))
              .filter(num_ingredients=2)

1voto

whusterj Points 1139

Une autre approche du cas ET pour Django 1.11+ serait d'utiliser la relativement nouvelle fonction QuerySet intersection() méthode. D'après la documentation, cette méthode :

Utilise le langage SQL INTERSECT pour retourner les éléments partagés de deux QuerySets ou plus.

Donc, étant donné une liste arbitraire de IngredientType des clés primaires, vous pouvez créer un filter() pour chaque pk (appelons-les subqueries ), puis de diffuser cette liste (le * ) dans le intersection() méthode.

Comme ça :

# the base `QuerySet` and `IngredientType` pks to filter on
queryset = Recipe.objects.all()
ingredient_type_pks = [3, 7]

# build the list of subqueries
subqueries = []
for pk in ingredient_type_pks:
    subqueries.append(queryset.filter(ingredients__ingredient_type__pk=pk))

# spread the subqueries into the `intersection` method
return queryset.intersection(*subqueries).distinct()

J'ai ajouté distinct() juste pour être sûr et éviter les résultats en double, mais je ne suis pas certain que ce soit nécessaire. Je vais devoir tester et mettre à jour ce post plus tard.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X