假设:
class Video(models.Model):
tags = models.ManyToManyField(Tag)
class Tag(models.Model):
name = models.CharField(max_length=20)
我知道我可以使用Video.objects.filter(tags__name__in=['foo','bar'])
查找包含Videos
或 foo
标记的所有bar
,但为了找到这些标记有foo
AND bar
,我必须加入外键两次(如果我手写SQL)。有没有办法在Django中实现这个目标?
我已经尝试了.filter(Q(tag__name='foo') & Q(tag__name='bar'))
,但这只会创建一个不可能满足的查询,其中一个Tag
同时包含foo
和bar
。< / p>
答案 0 :(得分:1)
这不像它看起来那样直截了当。此外JOIN
两次使用同一个表通常不是一个好主意:想象一下你的列表包含十个元素。你要去JOIN
十次吗?这很容易变得不可行。
然而,我们可以做的是计算重叠。因此,如果给出一个元素列表,我们首先要确保这些元素是唯一的:
tag_list = ['foo', 'bar']
tag_set = set(tag_list)
接下来,我们计算实际在集合中的Video
的标签数量,然后检查该数字是否与我们集合中的元素数量相同,如:
from django.db.models import Q
Video.objects.filter(
Q(tag__name__in=tag_set) | Q(tag__isnull=True)
).annotate(
overlap=Count('tag')
).filter(
overlap=len(tag_set)
)
请注意,Q(tag__isnull-True)
用于在没有标记的情况下启用Video
s 。这可能看起来没必要,但如果tag_list
为空,我们就想获得所有视频(因为它们共有零个标签)。
我们还假设Tag
的名称是唯一,否则某些标签可能会被计算两次。
在幕后,我们将执行如下查询:
SELECT `video`.*, COUNT(`video_tag`.`tag_id`) AS overlap
FROM `video`
LEFT JOIN `video_tag` ON `video_tag`.`video_id` = `video`.`id`
LEFT JOIN `tag` ON `tag`.`id` = `video_tag`.`tag_id`
WHERE `tag`.`name` IN ('foo', 'bar')
GROUP BY `video`.`id`
HAVING overlap = 2