您好,我今天遇到了一些非常有趣的关于 NullBooleanField 和用于过滤 db 中的空值的空 ('', 'None') 值。
作为记录,我使用 django rest 框架。
这是我的经验:
我创建了一个 Django 模型:
class MyModel(models.Model):
is_solved = models.NullBooleanField()
在 django rest 框架中,我声明了我的视图:
class MyModelFilter(filters.FilterSet):
class Meta:
model = MyModel
fields = ('is_solved',)
class MyModelViewSet(ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_class = MyModelFilter
有了这个,我在字段is_solved
上使用 django rest 框架进行了过滤。
过滤器使用带有标签/值的选择小部件:
choices = (('', _('Unknown')),
('true', _('Yes')),
('false', _('No')))
如此处所述: https ://github.com/carltongibson/django-filter/blob/develop/django_filters/widgets.py#L103
当我使用此过滤器时,如果我选择Unknown
,则会返回所有内容......所以没有过滤......
我想要的是在db中只有is_solved
设置为null
的对象。
此外,我想改变is_solved
的显示方式,所以我对我的模型做了一些修改。
新模式:
CHOICES = (
(None, "OFF"),
(True, "YES"),
(False, "NO")
)
class MyModel(models.Model):
is_solved = models.NullBooleanField(choices=CHOICES)
通过此修改,django rest 框架中的过滤器为我提供了正确的 Select Widget,但它现在使用具有自己的小部件的 ChoiceField。
这也更有意义,因为现在我在创建模型而不是文本输入时有一个选择框。
但是当我选择OFF
进行过滤时,url 将 query_parameter 设置为?is_solved=
:/
无论如何,它适用于 True 和 False。
因此,为了使其与 django rest 框架一起使用,我将选择更改为:
CHOICES = (
('None', "OFF"),
(True, "YES"),
(False, "NO")
)
注意'None'
而不是None
。 没关系,我现在有一个查询参数?is_solved=None
但是现在,这个过滤器没有返回任何东西......列表是空的......
让我把手伸进 django_filters 代码:)
关键在这部分代码中:
https://github.com/carltongibson/django-filter/blob/develop/django_filters/filters.py#L172
def filter(self, qs, value):
if isinstance(value, Lookup):
lookup = six.text_type(value.lookup_type)
value = value.value
else:
lookup = self.lookup_expr
if value in EMPTY_VALUES:
return qs
if self.distinct:
qs = qs.distinct()
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
return qs
这段小代码进行过滤。
我发现 dkango QuerySet 行为非常有趣。
其中重要的几行是:
if value in EMPTY_VALUES:
return qs
如果我们有一个空值,即[], (), u'', {}, None
,则返回非过滤查询
但是我们的值是'None'
所以没关系,所有的先例代码都没有将'None'
值转换为None
。 但是使用没有choices
的NullBooleanField
#$ 会做一些完全不同的事情,因为该字段不被视为 ChoiceField :
来自 django 核心的代码:
class NullBooleanField(BooleanField):
"""
A field whose valid values are None, True and False. Invalid values are
cleaned to None.
"""
widget = NullBooleanSelect
def to_python(self, value):
"""
Explicitly checks for the string 'True' and 'False', which is what a
hidden field will submit for True and False, for 'true' and 'false',
which are likely to be returned by JavaScript serializations of forms,
and for '1' and '0', which is what a RadioField will submit. Unlike
the Booleanfield we need to explicitly check for True, because we are
not using the bool() function
"""
if value in (True, 'True', 'true', '1'):
return True
elif value in (False, 'False', 'false', '0'):
return False
else:
return None
def validate(self, value):
pass
现在事情变得非常有趣......
看看这一行:
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
它进行过滤以传递给 django core db Queryset。
我们可以将其翻译为:
qs = qs.filter(is_solved__exact='None')
此操作将返回一个空查询集...
它将产生一个带有is_solved = None
的 sql 查询
但是如果我们测试:
qs = qs.filter(is_solved__exact=None)
它仅正确返回 db 中具有null
值的实例。
它将产生一个带有is_solved IS NULL
的 sql 查询
这是完全相同的测试:
qs = qs.filter(is_solved__isnull=True)
但是, 'True'
和'False'
值没有这个问题。 Django 的转录是正确的。
我们可以考虑用isnull
覆盖过滤器的 lookup_epxr,但我们的过滤器不适用于True
和False
值...
在这一点上,我真的不知道如何处理这种行为,django bug? django过滤器遗漏了什么?
无论如何,我已经能够通过重写 django 过滤器的filter
函数来使其工作:
def filter(self, qs, value):
from django.db.models import NullBooleanField
if isinstance(value, Lookup):
lookup = six.text_type(value.lookup_type)
value = value.value
else:
lookup = self.lookup_expr
if value in EMPTY_VALUES:
return qs
if self.distinct:
qs = qs.distinct()
# if original field is a NullBooleanField, we need to transform 'None' value as None
if isinstance(self.model._meta.get_field(self.name), NullBooleanField) and value in ('None',):
value = None
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
return qs
这看起来像一个黑客,它现在不让我高兴......
如果有人有兴趣思考这个问题,我会很高兴听到的! :D
现在,对这个修改有什么想法吗?
现在,我想我将使用带有选择选项的IntegerField
来不必使用分叉版本的 django 过滤器...因为我认为这不是 django 过滤器问题的开始:)
谢谢,
嗨@GuillaumeCisco - 这里似乎存在三个问题:
BooleanWidget
不适用于NullBooleanField
s,因为它不验证空值。可能应该有一个NullBooleanWidget
来容纳附加选项。
这可能最好通过子类化小部件类并手动设置choices
属性来处理。 BooleanWidget
不能真正接受选择参数,因为它需要特定的值( 'true'
/ 'false'
而不是True
/ False
) . 这同样适用于潜在的NullBooleanWidget
。
None
过滤存在问题,因为它没有通过空值检查。这里需要一个神奇的值,例如'None'
或'null'
。 filter()
方法必须考虑到这一点。
我认为#519 涵盖了大部分内容。
@GuillaumeCisco请查看。 如果有您认为应该涵盖的特定测试用例,您是否可以打开 PR 添加该测试用例,我们可以进行审核。
谢谢。
谢谢@carltongibson ,也谢谢@rpkilby
刚刚拉了开发分支。
NullBooleanField 现在正在使用具有此配置的 django rest 框架:
Model
:
CHOICES = (
(None, "OFF"),
(True, "YES"),
(False, "NO")
)
class MyModel(models.Model):
is_solved = models.NullBooleanField(choices=CHOICES)
View
:
choices = (
(True, "YES"),
(False, "NO")
)
class MyModelFilter(filters.FilterSet):
is_solved = ChoiceFilter(null_label='OFF', choices=choices)
class Meta:
model = MyModel
fields = ('is_solved',)
class MyModelViewSet(ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_class = MyModelFilter
注意选择元组之间的区别。
最有用的评论
谢谢@carltongibson ,也谢谢@rpkilby
刚刚拉了开发分支。
NullBooleanField 现在正在使用具有此配置的 django rest 框架:
Model
:View
:注意选择元组之间的区别。