在Django的ORM中使用相同的参数链接Q()对象时遇到麻烦

时间:2018-10-27 23:01:23

标签: django django-queryset

我正在努力创建鸡尾酒食谱应用程序作为学习练习。

我试图通过Django的Rest Framework创建一个过滤器,该过滤器通过查询参数(?= ingredients_exclusive = 1,3,4)接受一串成分ID,然后搜索具有所有这些成分的所有配方。我想搜索“所有含有朗姆酒和石榴汁糖浆的鸡尾酒”,然后分别搜索“所有含有朗姆酒的鸡尾酒和所有含有龙胆糖的鸡尾酒。”

我的应用程序中的三个模型是Recipes,RecipeIngredients和IngredientTypes。食谱(老式)具有多种RecipeIngredients(2盎司威士忌),并且RecipeIngredients均为成分类型(威士忌)。最终,我将决定将RecipeIngredient更改为a through model,具体取决于我决定采取的程度。

列表的长度可以可变,因此我不能仅将过滤器功能链接在一起。我必须遍历id列表,然后构建一个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对象为什么与同一对象的链接过滤器不同?

我已经阅读了Django文档中的“ Complex lookups with Q objects”,但似乎无济于事。

仅供参考,这是我在Filters.py中的过滤器。

此命令的“ OR”版本正常运行:

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 :(得分:1)

Spanning multi-valued relationships中描述了两种filter()方法之间的区别:

  

单个filter()调用中的所有内容将同时应用,以过滤出符合所有这些要求的项目。...对于多值关系,它们适用于链接到主模型的任何对象,而不一定适用于那些是由先前的filter()通话选择的。

文档中的示例更加清楚。我会根据您的问题进行重写:

  

要选择同时包含3类型和7类型成分的所有食谱,我们将编写:

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

在您的模型中这当然是不可能的,因此这将返回一个空的查询集,就像您的Q示例与AND一样。

  

要选择所有包含3类型的配料以及{strong> 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)

答案 1 :(得分:1)

针对Django 1.11+的AND案例的另一种方法是使用相对较新的QuerySet intersection()方法。根据文档,此方法:

  

使用SQL的 INTERSECT 运算符返回两个或更多查询集的共享元素。

因此,给定IngredientType主键的任意列表,您可以为每个pk创建一个filter()查询(我们将其称为subqueries),然后传播该列表({{1 }}运算符)添加到*方法中。

像这样:

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() 只是为了安全起见并避免重复结果,但实际上我不确定是否有必要。以后必须测试和更新此帖子。