在Django QuerySet中,如何在多对一关系中过滤"不存在"

Kry*_*ski 52 django django-models django-orm

我有两个这样的模型:

class User(models.Model):
    email = models.EmailField()

class Report(models.Model):
    user = models.ForeignKey(User)
Run Code Online (Sandbox Code Playgroud)

实际上,每个模型都有更多的字段,这些字段与此问题无关.

我想过滤所有拥有以"a"开头并且没有报告的电子邮件的用户.将有更多.filter().exclude()基于其他领域的标准.

我想这样做:

users = User.objects.filter(email__like = 'a%')

users = users.filter(<other filters>)

users = ???
Run Code Online (Sandbox Code Playgroud)

我想要 ???过滤掉没有与之关联的报告的用户.我该怎么做?如果这不可能像我提出的那样,那么另一种方法是什么?

Ala*_*air 84

使用isnull.

users_without_reports = User.objects.filter(report__isnull=True)
users_with_reports = User.objects.filter(report__isnull=False).distinct()
Run Code Online (Sandbox Code Playgroud)

使用时isnull=False,distinct()需要防止重复结果.

  • 这没关系,但是在`__isnull = True`和`__isnull = False`的情况下,它会生成`OUTER JOIN`和`report`.对于有报告的用户的问题,它可能比"INNER JOIN"效率低.我确实发现了一个丑陋的黑客:`User.objects.filter(report__id__gt = 0).distinct()`.这假设ID> 0,不一定是这种情况.任何更好的强迫内部联接的方式,任何人? (8认同)
  • @Alasdair是的,使用“WHERE NOT EXISTS”几乎总是比“DISTINCT ... OUTER JOIN”更快,因为它避免了可能巨大的中间结果集。 (2认同)

Sto*_*ica 24

Django 1.11中的新增功能,您可以添加EXISTS子查询:

User.objects.annotate(
    no_reports=~Exists(Reports.objects.filter(user__eq=OuterRef('pk')))
).filter(
    email__startswith='a',
    no_reports=True
)
Run Code Online (Sandbox Code Playgroud)

这会生成如下所示的SQL:

SELECT
    user.pk,
    user.email,
    NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0.user = user.pk) AS no_reports
FROM user
WHERE email LIKE 'a%' AND NOT EXISTS (SELECT U0.pk FROM reports U0 WHERE U0.user = user.pk);
Run Code Online (Sandbox Code Playgroud)

一个NOT EXISTS条款几乎总是做一个"不存在"过滤器的最有效的方式.

一旦#25367发布,您将能够~Exists()直接在a中使用.filter(),避免重复条款.

  • 这应该是最佳答案。谢谢@OrangeDog。 (4认同)

Yur*_*rov 11

在没有额外查询或JOIN的情况下获取本机SQL EXISTS/NOT EXISTS的唯一方法是在.extra()子句中将其添加为原始SQL:

users = users.extra(where=[
    """NOT EXISTS(SELECT 1 FROM {reports} 
                  WHERE user_id={users}.id)
    """.format(reports=Report._meta.db_table, users=User._meta.db_table)
])
Run Code Online (Sandbox Code Playgroud)

事实上,这是一个非常明显和有效的解决方案,我有时想知道为什么它不是作为查找内置到Django.此外,它还允许细化子查询以查找例如仅在上周用[out]报告的用户,或者[out]未答复/未查看的报告.

  • @OrangeDog参数不能用于传递表名。它们将由数据库引擎引用。另外,额外保护表名也没有用,因为它们来自代码,而不是来自用户输入。 (2认同)
  • @OrangeDog - 因为大多数数据库引擎将单引号解释为字符串文字 - 表需要双引号/角引号,或者根本不引号 (2认同)

nev*_*ner 6

除了@OrangeDog 的回答。从 Django 3.0 开始,您可以使用Exists子查询直接过滤查询集:

User.objects.filter(
    ~Exists(Reports.objects.filter(user__eq=OuterRef('pk'))
)
Run Code Online (Sandbox Code Playgroud)