Проблема с цепочкой объектов Q () с одним и тем же аргументом в ORM Джанго - PullRequest
0 голосов
/ 28 октября 2018

Я работаю над созданием приложения рецепта коктейля в качестве учебного упражнения.

Я пытаюсь создать фильтр через Rest Framework Django, который принимает строку идентификаторов ингредиентов через параметр запроса (? = Ингридиенты_эксклюзив = 1,3,4), а затем ищет все рецепты, содержащие все эти ингредиенты. Я хотел бы найти «Все коктейли с ромом и гренадином», а затем отдельно «Все коктейли с ромом и все коктейли с гренадином».

Три модели в моем приложении - Recipes, RecipeIngredients и IngredientTypes. Рецепты (старомодные) имеют несколько ингредиентов RecipeIngredients (2 унции виски), а все ингредиенты RecipeIngredients имеют тип ингредиента (виски). В конце концов я изменю RecipeIngredient на через модель в зависимости от того, как далеко я решу это сделать.

Список может быть переменной длины, поэтому я не могу просто соединить вместе функции фильтра. Я должен пройтись по списку идентификаторов, а затем построить Q ().

Однако у меня есть некоторые проблемы. С помощью Django Shell я сделал это:

>>> 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>]>

Итак, вот мой вопрос: почему объект Q, который И выполняет два запроса, отличается от сцепленных фильтров одного и того же объекта?

Я прочитал " Сложные поиски с объектами Q " в документации Django, и, похоже, это не помогло.

Просто для справки, вот мои фильтры в Filters.py.

Версия этой команды «ИЛИ» работает правильно:

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 = ()

Я также включил мои модели ниже:

# -*- 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}'

Любая помощь будет принята с благодарностью!

Ответы [ 2 ]

0 голосов
/ 30 октября 2018

Другим подходом к случаю AND для Django 1.11+ было бы использование относительно нового метода QuerySet intersection () .Согласно документам, этот метод:

Использует оператор SQL INTERSECT для возврата общих элементов двух или более наборов QuerySets.

Итак, учитываяпроизвольный список первичных ключей IngredientType, вы можете создать запрос filter() для каждого pk (давайте назовем их subqueries) и затем распространить этот список (оператор *) в метод intersection().

Примерно так:

# 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()

Я добавил туда distinct(), чтобы быть в безопасности и избежать повторяющихся результатов, но на самом деле я не уверен, нужно ли это.Придется проверить и обновить этот пост позже.

0 голосов
/ 28 октября 2018

Разница между двумя подходами к filter() описана в Включение многозначных связей :

Все внутри одного filter()вызов применяется одновременно, чтобы отфильтровать элементы, соответствующие всем этим требованиям .... Для многозначных отношений они применяются к любому объекту, связанному с основной моделью, не обязательно к тем объектам, которые были выбраны более ранним вызовом filter().

Пример в документации делает это более понятным.Я перепишу это с точки зрения вашей проблемы:

Чтобы выбрать все рецепты, содержащие ингредиенты с типом 3 и типом 7, мы написали бы:

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

Это, конечно, невозможно в вашей модели, так что это вернет пустой набор запросов, как ваш Q пример с AND.

Чтобы выбрать все рецепты, которые содержатингредиент с типом 3 , а также ингредиент с типом 7, мы бы написали:

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

Это не особенно интуитивно, но им нужен был способчтобы различать эти два случая и вот что они придумали.


Возвращаясь к вашей проблеме, случай OR можно упростить с помощью оператора in:

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

Случай AND сложен, потому что это условие, которое включает несколько строк.Простой подход заключается в том, чтобы просто взять приведенную выше версию OR и продолжить ее обработку в Python, чтобы найти подмножество, содержащее все ингредиенты.

Подход на основе запроса, который должен работать, включает аннотацию с Count.Это не проверено, но что-то вроде:

Recipe.objects.annotate(num_ingredients=Count("ingredients", 
                            filter=Q(ingredients__ingredient_type__in=[3, 7]))
              .filter(num_ingredients=2)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...