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

Wes*_*ley 1 django django-models django-3.2

在可以为空的外键上过滤查询集时,我可以按 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)
Run Code Online (Sandbox Code Playgroud)
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!
Run Code Online (Sandbox Code Playgroud)

Ama*_*dan 5

关键的见解是,在 SQL 中,NULL代表未知值,无法使用普通运算符进行比较:

SELECT NULL = NULL;
-- => NULL
Run Code Online (Sandbox Code Playgroud)

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

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

因此,aNULL不等于另一个NULL...但它也与另一个没有不同NULLNULL <> NULL也返回NULL。事实上,NULL感染所有运算符:1 + NULL1 < NULL1 >= NULL... 全部结果为NULL。如果您对未知值执行任何操作,则结果也是未知值。

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

SELECT NULL IS NULL;
-- => t
Run Code Online (Sandbox Code Playgroud)

与 一样x = NULLx IN (NULL)也使用相同的相等比较,它永远不能评估为 true:

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
Run Code Online (Sandbox Code Playgroud)

2没有?Maaaaybe;我有一个值,我不知道它是什么,所以我不能说它是否2在那里,因为它可能与那个未知的值匹配。那我不知道的另一件事怎么样?打败我的是,可能是1,或者可能等于其他未知的东西——或者它可能是完全不同的东西。怎么样1?嗯,至于这一点,我可以看到1那里,无论未知的东西是什么或不是什么。

IN因此,您必须明确检查,而不是 plain NULL

SELECT * WHERE foo_id IN (1, 2) OR foo_id IS NULL;
Run Code Online (Sandbox Code Playgroud)

用 Django 术语来说:

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

或者如果你有多个值

Bar.objects.filter(Q(foo_id__in=[1, 2]) | Q(foo_id__isnull=True)).count()
Run Code Online (Sandbox Code Playgroud)