【问题标题】:Django filter queryset __in for *every* item in list (2.0)Django过滤查询集__in用于列表中的*每个*项目(2.0)
【发布时间】:2017-11-07 20:29:59
【问题描述】:

我已经阅读了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)    

我想这样做:“选择所有具有所有的配方”的配料数组的配料。例如:“经典香草蛋糕”有这些成分:鸡蛋、万能面粉、发酵粉、小苏打、黄油、糖、香草、酪乳。如果缺少一个,则“经典香草蛋糕”不应出现在结果查询中。相反,如果有更多种成分超过所需成分,则“经典香草蛋糕”应始终出现在结果查询中。

到目前为止,我已经这样做了,但它不起作用。

    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

我该怎么做?

【问题讨论】:

  • 我的问题还不是很清楚。你想得到一个包含所有成分的食谱 ​​r(例如成分 ID)?
  • 我认为您只是想将注释放在过滤器之前,以便每个食谱都带有其包含的成分数量的注释。
  • @Sagar 我更新了我的问题:我的冰箱里有一些配料,它们的 id (= id 数组)。我想看看我能用它做什么。我想比较一个数字,它是配方本身所有成分的总和。

标签: django django-models django-queryset


【解决方案1】:

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

  • 首先,过滤配方中的成分,对于每个配方 (= 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;

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 您的最新建议非常聪明,应该可行。不行吗?
    • @OlivierPons 我认为它由于错误而无法正常工作。我现在做了一些测试,它适用于 ForeignKeys,但在 M2M 字段中失败。
    • 我刚刚在 django-users 上开了一个新帖子,你可以查看它的进度here
    • 非常感谢您的操作,我希望这是一个实际的错误,并且会尽快解决!
    • @OlivierPons 你试过Subquery 方法吗?有效吗?
    猜你喜欢
    • 2020-09-14
    • 2018-10-31
    • 2015-12-30
    • 1970-01-01
    • 2018-10-20
    • 1970-01-01
    • 1970-01-01
    • 2021-10-20
    • 2019-08-08
    相关资源
    最近更新 更多