为什么 Django 外键 id __in 查询无法匹配 None?

Why does a Django foreign key id __in query fail to match None?

在可空外键上过滤查询集时,我可以按 ID 值 (foo_id=123) 或 None (foo_id=None) 进行过滤。但是,如果我尝试按列表 (foo_id__in=[123, None]) 进行过滤,则会忽略 None

为什么会这样,使用包含 None 的列表过滤外键的最佳解决方法是什么?

示例:

from django.db import models

class Foo(models.Model):
  name = models.CharField(max_length=100)

class Bar(models.Model):
  foo = models.ForeignKey(Foo, on_delete=models.PROTECT,
                          blank=True, null=True)
foo = Foo.objects.create(name='myfoo')
Bar.objects.create(foo=foo)
Bar.objects.create(foo=None)

Bar.objects.count()                                    # 2
Bar.objects.filter(foo_id=foo.id).count()              # 1
Bar.objects.filter(foo_id=None).count()                # 1
Bar.objects.filter(foo_id__in=[foo.id, None]).count()  # 1 - Expected 2!

我不知道为什么 foo_id__in=[123, None] 中的 None 会被忽略,但我想出的最佳解决方法如下:

Bar.objects.filter(Q(foo_id=foo.id) | Q(foo_id=None)).count()

关键在于,在SQL中,NULL表示一个未知值,无法使用普通运算符进行比较:

SELECT NULL = NULL;
-- => NULL

(此语法不适用于所有数据库引擎 - 例如,SQL 服务器 - 在这些引擎中你必须编写类似 SELECT CASE WHEN NULL = NULL THEN 't' ELSE 'f' END 的内容,但结果是相同的:NULL = NULL 计算为 NULL,这是错误的。)

推理是,例如,如果您有两个不知道姓氏的人,您会将他们标记为 NULL - 因为他们都是 NULL,你不能断定他们有相同的姓氏(就像你不能断定他们有不同的姓氏一样 - 你只是不知道其中一种方式)。

因此,NULL 不等于另一个 NULL...但它也与另一个 NULL 没有区别:NULL <> NULL 也 returns NULL。事实上,NULL 会感染所有运算符:1 + NULL1 < NULL1 >= NULL... 都会导致 NULL。如果您对未知值执行任何操作,结果就是一个未知值。

基本上只有一个运算符可以避免NULL的这种传染性,那就是IS NULL:

SELECT NULL IS NULL;
-- => t

x = NULL一样,x IN (NULL)也使用相同的相等比较,永远不能计算为真:

SELECT 2 IN (1, NULL);
-- => NULL
SELECT NULL IN (1, NULL);
-- => NULL
SELECT 1 IN (1, NULL);
-- => t

SELECT 2 NOT IN (1, NULL);
-- => NULL
SELECT NULL NOT IN (1, NULL);
-- => NULL
SELECT 1 NOT IN (1, NULL);
-- => t

2在吗? Maaaybe;我有一个我不知道它是什么的值,所以我不能说 2 是否在那里,因为它可能匹配那个未知的值。我不知道的另一件事怎么样?打败我,可能是 1,或者可能等于另一个未知的东西——或者它可能是完全不同的东西。 1 怎么样?好吧,关于那个,我可以在那里看到 1,不管未知的东西是什么或不是什么。

因此,您必须显式检查 NULL,而不是普通的 IN

SELECT * WHERE foo_id IN (1, 2) OR foo_id IS NULL;

在 django 术语中:

from django.db.models import Q
Bar.objects.filter(Q(foo_id=foo.id) | Q(foo_id__isnull=True)).count()

或者如果您有多个值

Bar.objects.filter(Q(foo_id__in=[1, 2]) | Q(foo_id__isnull=True)).count()