Django查询多对多子集包含

Posted

技术标签:

【中文标题】Django查询多对多子集包含【英文标题】:Django query for many-to-many subset containment 【发布时间】:2014-04-04 00:29:45 【问题描述】:

有没有办法查询包含多对多字段的子集或超集?

假设每个人都有一个他们想看的鸟的列表,每个鸟舍都有一个鸟的列表。对于给定的 Person 实例,我如何进行查询以查找该人的列表中有每只鸟 的 Aviaries?同样,对于给定的 Person 实例,我如何找到哪些 Aviaries 在此人的列表中只有只鸟类(但不一定是所有鸟类)。

这是我的 Django 1.5 模型:

class Bird(models.Model):
    name = models.CharField(max_length=255, unique=True)

class Aviary(models.Model):
    name = models.CharField(max_length=255, unique=True)
    birds = models.ManyToManyField(Bird)

class Person(models.Model):
    name = models.CharField(max_length=255, unique=True)
    birds_to_see = models.ManyToManyField(Bird)

我知道我将如何找到至少有一个人的鸟类的鸟舍,但我不知道我将如何适应这里。 (例如,参见: django queryset for many-to-many field)

如果有一个查询可以执行我想要的操作,我也很想知道是否/为什么更“手动”执行此操作更可取。例如,我可以遍历 aviaries,提取每个 aviary 的鸟类列表,然后查看此人的 bird_to_see 是 aviary 鸟类列表的子集还是超集:

def find_aviaries(self):
    person_birds = set(self.birds_to_see.all())
    found_aviaries = []
    for aviary in Aviary.objects.all():
        aviary_birds = set(aviary.birds.all())
        if person_birds.issubset(aviary_birds):
            found_aviaries.append(aviary)            
    return found_aviaries

感谢任何帮助!

【问题讨论】:

【参考方案1】:

Django >= 2.0 存在一个很好的解决方案。可以通过匹配鸟类的数量来注释 Aviaries,并过滤与至少一个 Bird 或所需数量匹配的 Aviaries。

from django.db.models import Count

    ...
    person_birds = set(self.birds_to_see.all())
    aviaries = (
        Aviary.objects
        .annotate(bird_match_count=Count('birds', filter=Q(birds__in=person_birds)))
        .filter(bird_match_count__gt=0)
    )

然后通过bird_match_count=len(person_birds) 过滤新查询集或在Python 中过滤原始查询集或按bird_match_count 对其进行排序是很简单的。

Django AviaryBirds 并且会更加冗长。


已通过读取 SQL 验证

>>> print(aviaries.query)
SELECT aviary.id, aviary.name,
  COUNT(CASE WHEN  aviary_birds.bird_id IN (1,..)  THEN aviary_birds.bird_id ELSE NULL END)
    AS bird_match_count
FROM aviary LEFT OUTER JOIN aviary_birds ON (aviary.id = aviary_birds.aviary_id)
GROUP BY aviary.id, aviary.name
HAVING
  COUNT(CASE WHEN (aviary_birds.bird_id IN (1,..)) THEN aviary_birds.bird_id ELSE NULL END)
     > 0

【讨论】:

您应该能够引用Aviary.birds.through 以获取对旧版本 Django 中“通过”模型的引用。 @MatthewSchinckel 是的。我在关于 Django 【参考方案2】:

使用 Postgres 子查询数组构造,您可以对 id 进行注释,然后进行相应的过滤:

birds = Aviary.birds.through.objects.filter(
    aviary=OuterRef('pk')
).values('bird')
aviaries = Aviary.objects.annotate(
    bird_ids=SubqueryArray(birds)
).filter(bird_ids__contains=target_bird_ids)

您也可以使用__contained_by 换另一条路(如果您只想匹配,也可以使用__overlap)。

那么你只需要一个合适的SubqueryArray 类:

class SubqueryArray(django.db.models.expressions.Subquery):
    template = 'ARRAY(%(subquery)s)'
    output_field = ArrayField(base_field=models.CharField())

您可能需要调整其输出字段,具体取决于您的 PK 字段。

【讨论】:

(后人注意:在提出原始问题时,此解决方案并不存在 - 该功能直到很久以后才实现)。

以上是关于Django查询多对多子集包含的主要内容,如果未能解决你的问题,请参考以下文章

多对多查询的 Django 模型文件更新

Django查询集获得精确的多对多查询[重复]

Django - 查询列表中的任何项目在多对多字段中的任何对象

django - 如何查询仅描述的对象存在于多对多字段中的位置

查询 django 多对多

逻辑或 Django 多对多查询返回重复结果