Django过滤查询集__in for * * *列表中的项目(2.0)

时间:2017-06-06 17:43:04

标签: django django-models django-queryset

我已经阅读了thisthis,但它们并没有解决我的问题,因为它们通过与硬编码的数字进行比较来进行最终的“计数”。我想比较一个数字,它是配方本身所有成分的总和。

让我们想象一下,我的冰箱中有一些成分,带有id(= id的数组)。我想看看我能做些什么。我的模特是这样的:

class Ingredient(models.Model):
    label = models.CharField(max_length=200, null=True, blank=True,
                             default=None)
    description = models.TextField(null=True, blank=True, default=None)


class Unit(models.Model):
    label = models.CharField(max_length=200, null=True, blank=True,
                             default=None)
    abbr = models.CharField(max_length=20, null=True, blank=True,
                            default=None)


class IngredientUnit(models.Model):
    ingredient = models.ForeignKey(Ingredient, null=False, blank=True)
    unit = models.ForeignKey(Unit, null=False, blank=True)
    measurable = models.BooleanField(default=True, null=False, blank=True)
    is_int = models.BooleanField(default=True, null=False, blank=True)
    value = models.FloatField(null=True, blank=True, default=0.0)    


class Recipe(models.Model):
    label = models.CharField(max_length=200, null=True, blank=True,
                             default=None)
    description = models.TextField(null=True, blank=True, default=None)
    ingredients = models.ManyToManyField(IngredientUnit)    

我想这样做:'选择所有具有 all 的配方'成分pk的成分。例如:“经典香草蛋糕”有这些成分:鸡蛋,通用面粉,发酵粉,小苏打,黄油,糖,香草,酪乳。如果缺少一个,“经典香草蛋糕”不应该出现在结果查询中。相反,如果有更多成分而不是那些,那么“经典香草蛋糕”应该始终存在于结果查询中。

到目前为止,我已经这样做了,但它没有用。

    ingredient_ids = self.request.POST.getlist('ingredient[]', [])
    if len(ingredient_ids):
        recipes = Recipe.objects\
            .filter(ingredients__in=ingredient_ids)\
            .annotate(nb_ingredients=Count('ingredients'))\
            .filter(nb_ingredients=len(ingredient_ids))
        print([a for a in recipes])

问题是nb_ingredients=len(ingredient_ids)应为nb_ingredients=the number of the ingredients of the current recipe

我该怎么做?

2 个答案:

答案 0 :(得分:0)

我认为您可以尝试排除缺少成分的食谱。

Recipe.objects.exclude(~Q(ingredients__in=ingredient_ids)).distinct()

测试它,它不起作用。

理论上,你想要的是什么:

  

问题是nb_ingredients = len(ingredient_ids)应该是   nb_ingredients =当前食谱的成分数

应该在过滤器之前使用一个注释,在过滤器之后使用一个注释(因为the order of annotation and filter matters)。

即。像这样的东西:

recipes = (Recipe.objects
    .annotate(num_ingredients=Count('ingredients'))
    .filter(ingredients__in=ingredient_ids)
    .annotate(found_ingredients=Count('ingredients'))
    .filter(num_ingredients=F('found_ingredients'))
)

然而,当我测试它时,它没有工作,因为ORM没有从过滤器重用正确的JOIN。这可能是某种错误,我只是在邮件列表中打开question

如果您使用的是Django 1.11,另一种方法是使用新的Subquery-expressions,您可以试试吗。

recipes = (Recipe.objects
                .filter(ingredients__in=ingredient_ids)
                .annotate(found_ingredients=Count('ingredients', distinct=True))
                .annotate(num_ingredients=Count(Subquery(
                    Ingredient.objects.filter(recipe=OuterRef('pk')).only('pk')),
                    distinct=True)
                )
                .filter(num_ingredients=F('found_ingredients'))
)

答案 1 :(得分:0)

我找到了!不能避免双重查询,但它就像一个魅力。这是解决方案:

  • 首先,对每个食谱(= group by)过滤食谱中的成分,计算找到的成分总数
  • 然后对于所有现有的食谱,如果成分的总数==之前发现的成分总量,那么它就可以了,保留它。

感觉就像使用大锤来破解坚果(即使第一个查询过滤并消除了很多食谱),但是如果你有一个更好的解决方案,那就是你的男人!

recipes = Recipe.objects \
    .annotate(found=Count('*'))\
    .filter(ingredients__in=ingredient_ids)
for recipe in recipes:
    a = Recipe.objects.annotate(total=Count('ingredients')).filter(
        pk=recipe.pk, total=recipe.found)
    print("Recipe found:", str(a))

例如,如果成分的ID为[1, 2, 3, 4, 5],您将获得这两个查询:

SELECT "app_recipe"."id", "app_recipe"."label", "app_recipe"."description",
    COUNT(*) AS "found" FROM "app_recipe"
INNER JOIN "app_recipe_ingredients"
ON ("app_recipe"."id" = "app_recipe_ingredients"."recipe_id")
WHERE "app_recipe_ingredients"."ingredientunit_id" IN (1, 2, 3, 4, 5)
GROUP BY "app_recipe"."id", "app_recipe"."label", "app_recipe"."description";

第二个循环将基于如下所示的配方进行查询:

SELECT "app_recipe"."id", "app_recipe"."label", "app_recipe"."description",
    COUNT("app_recipe_ingredients"."ingredientunit_id") AS "total" 
FROM "app_recipe" 
LEFT OUTER JOIN "app_recipe_ingredients" 
ON ("app_recipe"."id" = "app_recipe_ingredients"."recipe_id") 
WHERE "app_recipe"."id" = 1 
GROUP BY "app_recipe"."id", "app_recipe"."label", "app_recipe"."description"
HAVING COUNT("app_recipe_ingredients"."ingredientunit_id") = 5;