こんにちは、私は今日NullBooleanFieldとdbのnull値をフィルタリングするための空の( ''、 'None')値に関して非常に興味深いことに遭遇しました。
記録のために、私はdjangorestフレームワークを使用します。
これが私の経験です:
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
にdjangorestフレームワークのフィルターがあります。
フィルタは、ラベル/値を持つ選択ウィジェットを使用します。
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を使用するようになりました。
また、テキスト入力の代わりにモデルを作成するときにselectBoxがあるので、より理にかなっています。
しかし、フィルタリングに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
この小さなコードがフィルタリングを行います。
そして、dkangoQuerySetの動作で非常に興味深いものを見つけました。
重要な行の一部は次のとおりです。
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コアdbクエリセットに渡すフィルタリングを行います。
私たちはそれを次のように翻訳することができます:
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によって正しいです。
フィルタのlookup_epxrをisnull
でオーバーライドすることも考えられますが、フィルタはTrue
とFalse
の値では機能しません...
この時点で、私は本当にこの動作を処理する方法を知りません、djangoバグ? 何かが足りない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
とchoicesオプションを使用して、フォークバージョンのdjangoフィルターを使用する必要がないと思います...これはdjangoフィルターではないと思うので最初に発行します:)
ありがとう、
こんにちは@ GuillaumeCisco-ここには3つの問題があるようです:
BooleanWidget
は、null値を検証しないため、 NullBooleanField
では機能しません。追加のオプションに対応するNullBooleanWidget
があるはずです。
これは、ウィジェットクラスをサブクラス化し、 choices
プロパティを手動で設定することでおそらく最も適切に処理されます。 BooleanWidget
は、特定の値を期待するため、実際には選択肢の引数を受け入れることができません($ True
/ False
$の代わりに$ 'true'
/ 'false'
#$) 。 同じことが潜在的なNullBooleanWidget
にも当てはまります。
None
によるフィルタリングに問題があります。ここでは、 'None'
や'null'
などの魔法の値が必要になります。 filter()
メソッドはこれを考慮する必要があります。
これの大部分は#519でカバーされていたと思います。
@GuillaumeCiscoはそれを確認してください。 カバーする必要があると思われる特定のテストケースがある場合は、それを追加してPRを開き、レビューすることができます。
ありがとう。
@carltongibsonに感謝し、@ rpkilbyにも感謝します
開発ブランチを引っ張っただけです。
NullBooleanFieldは、次の構成でdjangorestフレームワークで動作しています。
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は、次の構成でdjangorestフレームワークで動作しています。
Model
:View
:選択肢のタプルの違いに注意してください。