基于ManyToMany关系存在过滤Django QuerySet

Posted

技术标签:

【中文标题】基于ManyToMany关系存在过滤Django QuerySet【英文标题】:Filtering Django QuerySet based on ManyToMany relationship existence 【发布时间】:2017-11-15 15:49:11 【问题描述】:

模型

这是我们的基本模型设置。

一个List有很多Item,一个Item可以在很多List中。对于给定的项目,如果 任何 个列表是 good(即list.bad == False),则该项目是 good。如果一个项目没有出现在任何 good 列表中,那么它就是 bad

我们有一个自定义的 Items QuerySet,有一个只返回好 Item 的方法,一个只返回坏 Item 的方法。

class Item(models.Model):
    objects = ItemQuerySet.as_manager()
    name = models.CharField(max_length=255, unique=True)

class List(models.Model):
    name = models.CharField(max_length=255, unique=True)
    bad = models.BooleanField(default=True)
    items = models.ManyToManyField(Item, related_name='lists')

class ItemQuerySet(models.QuerySet):
    def bad(self):
        return self.exclude(lists__bad=False)

    def good(self):
         return self.filter(lists__bad=False)

情景

这是一个我们遇到问题的场景示例:一个坏列表、一个好的列表和两个项目。

BadList:    GoodList:
- Item1     - Item1
- Item2

由于 Item1 至少出现在一个好的列表中,它应该出现在 Item.objects.good() 中,而不是 Item.objects.bad() 中。

由于 Item2 没有出现在任何好的列表中,它应该出现在 Item.objects.bad(),而不是 Item.objects.good()

我们可以这样设置场景:

# Create the two lists.
>>> goodlist = List.objects.create(name='goodlist', bad=False)
>>> badlist = List.objects.create(name='badlist', bad=True)

# Create the two items.
>>> item1 = Item.objects.create(name='item1')
>>> item2 = Item.objects.create(name='item2')

# Item1 goes in both lists
>>> goodlist.items.add(item1)
>>> badlist.items.add(item1)

# Item2 only in badlist
>>> badlist.items.add(item2)

确实,Item.objects.good()Item.objects.bad() 工作正常:

>>> Item.objects.bad() # This returns what we want! Good!
<QuerySet [<Item: item2>]>

>>> Item.objects.good() # This returns what we want! Good!
<QuerySet [<Item: item1>]>

问题

感谢您对我的包容。这是我们的自定义 QuerySet 出错的地方。如果我们通过单个 List 的 Item 访问 good()bad() 自定义 QuerySet 方法,我们会得到不正确的结果。

>>> badlist.items.bad() # WRONG! We want to ONLY see item2 here!
<QuerySet [<Item: item1>, <Item: item2>]

>>> badlist.items.good() # WRONG! We want to see item1 here!
<QuerySet []>

看起来,当我们执行badlist.items.bad() 时,查询 考虑badlist 来确定项目是否坏,而不是考虑项目所在的所有列表。但我'我很困惑为什么会这样。

我的想法是,在ItemQuerySet.bad 方法中,我想要self.exclude(any__lists__bad=False) 之类的东西,而不仅仅是self.exclude(lists__bad=False)。但当然 any__ 关键字实际上并不存在,我不确定如何在 Django QuerySet 中正确表达该逻辑。似乎使用Q 对象可能是前进的方向,但我仍然不太确定如何使用Q 对象表达这样的查询。

在我们的实际数据库中,Lists 不到 100 个,但 Item 数以百万计。因此,出于性能原因,最好使用一个查询而不是一个属性或多个查询来执行此操作。

干杯!

【问题讨论】:

【参考方案1】:

如果您打印出由badlist.items.bad() 生成的查询,您将看到问题:它将在直通表上使用WHERE 子句,从而将列表限制为仅坏列表。如果要正确应用badgood,则需要从Item级别开始,然后按列表中的项目进行过滤。

item_ids = list(badlist.items.values_list('id'), flat=True)

Item.objects.bad().filter(id__in=item_ids)

Item.objects.good().filter(id__in=item_ids)

编辑:我无法在没有架构的情况下对此进行测试,但我认为您可以使用注释来计算列表的数量,然后通过它进行过滤

def annotate_good(self);
    return self.annotate(good=Count(Case(When(lists__bad=False, then=1), default=0)))

def good(self):
    return self.annotate_good().exclude(good=0)

def bad(self):
    return self.annotate_good().filter(good=0)

否则,如果性能确实是个问题,我会在 Item 模型中添加一个好或坏的字段,并在保存时更新它,这样查询就变得非常简单。

【讨论】:

感谢您的回答!这确实可以正常工作,但是我担心其他使用该代码的人会尝试使用badlist.items.good() 路由,但不知道会更好。我想避免让人们被这样误导。另外,我想在原帖中提到这一点:在我们的实际数据库中,列表少于 100 个,但项目数百万。因此,出于性能原因,理想的做法是使用一个查询,而不是一个属性或多个查询。 啊,明白了。我认为您可以使用查询集注释来做到这一点。我用我认为应该可行的解决方案编辑了上面的答案。否则,为了提高性能,我只需将 bad/good 列添加到 Item 并更新它,这使得查询更加简单

以上是关于基于ManyToMany关系存在过滤Django QuerySet的主要内容,如果未能解决你的问题,请参考以下文章

在 Django Admin 中过滤多对多框

django - manytomany 上的查询过滤器为空

Django在ManyToMany计数上过滤模型?

Django:ManyToMany过滤器匹配列表中的所有项目

Django DRF视图过滤ManyToMany查询集

Django ManyToMany 关系详情详情