Django查询多对多子集包含关系

9 投票
2 回答
2376 浏览
提问于 2025-04-17 20:34

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

假设每个人都有一份他们想要看到的鸟的清单,而每个鸟舍里也有一份鸟的清单。那么,我该如何查询,找出对于某个特定的人来说,哪些鸟舍里有他们清单上的每一种鸟呢?同样,对于某个特定的人,我又该如何找出哪些鸟舍里只有他们清单上的鸟(但不一定是所有的)呢?

以下是我的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

如果有一个查询可以满足我的需求,我也想知道为什么这种方式比手动处理更好。例如,我可以遍历所有鸟舍,提取每个鸟舍的鸟的清单,然后查看这个人的鸟清单是否是鸟舍鸟清单的子集或超集:

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

任何帮助都非常感谢!

2 个回答

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

你可能需要根据你的主键字段调整输出字段。

3

对于Django 版本大于等于 2.0,有一个很不错的解决方案。你可以根据匹配的鸟类数量来标注鸟舍,并且可以筛选出至少有一只鸟或者符合特定数量的鸟舍。

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 中对原始查询结果进行过滤,甚至可以根据鸟类匹配数量来排序。

如果是Django 版本小于 2.0,就需要引用中间模型 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

撰写回答