为什么Django的外键id __in查询没有匹配到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!

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

SELECT NULL = NULL;
-- => NULL

(此语法并不适用于所有DB引擎-例如,SQL Server -在这些引擎中,您将不得不编写类似SELECT CASE WHEN NULL = NULL THEN 't' ELSE 'f' END的东西,但结果是相同的:NULL = NULL计算为NULL,这是假的。)

原因是,例如,如果你有两个人的姓你不知道,你会把他们标记为NULL-仅仅因为他们都是NULL,你不能断定他们有相同的姓(就像你不能断定他们有不同的姓-你只是不知道一种方式或另一种方式)。

所以,一个NULL不等于另一个NULL…但它也与另一个NULL没有什么不同:NULL <> NULL也返回NULL。事实上,NULL感染了所有的操作员:1 + NULL,1 < NULL,1 >= 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在吗?Maaaaybe;我有一个值,我不知道它是什么,所以我不能说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()

我不知道为什么foo_id__in=[123, None]中的None被忽略了,但我想出的最好的解决方案是:

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

最新更新