必須ではないCharFieldにunique制約をつけるとDuplicateErrorが発生するので,
入力がない場合に空文字ではなくNullを保存するFieldを作ろうとして少しハマったのでメモ書き.
保存時の処理
djangoではCharFieldやTextFieldを空で保存されるとNullではなく空文字で保存される.
IntegerFieldなどは空の場合にちゃんとNullが保存されているので何をしているかdjangoのソース確認すると
IntegerFieldクラスに以下のようなメソッドを発見.
def get_prep_value(self, value):
value = super(IntegerField, self).get_prep_value(value)
if value is None:
return None
return int(value)
これがDB保存前に値を変換するメソッドっぽいので,空文字の時にNoneを返してあげればNullを保存してくれるようになる.
class NullCharField(models.CharField):
def get_preg_value(self, value):
value = Super(NullCharField, self).get_preg_value(value)
return value if value else None
とりあえずこれでFormなどに入力がなかった場合にNullを保存してくれるようになる.
取得時の処理
この状態でadmin画面を開くとTypeErrorが発生する.
Exception Value: coercing to Unicode: need string or buffer, NoneType found
「stringかbuffer渡せや」って言われているのでNoneだったら空文字に変換する処理が必要っぽい.
to_stringメソッド書き換えればいいって思ってたけど,どうも上手くいかない.
色々調べているとメタクラスにdjango.db.models.SubfieldBaseを渡す必要があるらしい.
DBから取得した値はsetattrで入れてる(django.db.models.baseのModelクラスinitメソッド参照)が,
metaクラスにSubfieldBaseを渡すとsetattr時に内部でto_pythonメソッドが呼ばれるようになる.
というわけでmetaを指定し,取得時の処理を書き加えたのがこれ
from django.db import models
from django.utils import six
class NullCharField(six.with_metaclass(models.SubfieldBase, models.CharField)):
def get_preg_value(self, value):
value = super(NullCharField, self).get_preg_value(value)
return value if value else None
def to_python(self, value):
value = super(NullCharField, self).to_python(value)
return value if value is not None else u''
答え合わせ
なんかありそうって思って調べてたらoscarに普通にあった.
https://github.com/django-oscar/django-oscar/blob/master/src/oscar/models/fields/__init__.py
class NullCharField(six.with_metaclass(SubfieldBase, CharField)):
"""
CharField that stores '' as None and returns None as ''
Useful when using unique=True and forms. Implies null==blank==True.
When a ModelForm with a CharField with null=True gets saved, the field will
be set to '': https://code.djangoproject.com/ticket/9590
This breaks usage with unique=True, as '' is considered equal to another
field set to ''.
"""
description = "CharField that stores '' as None and returns None as ''"
def __init__(self, *args, **kwargs):
if not kwargs.get('null', True) or not kwargs.get('blank', True):
raise ImproperlyConfigured(
"NullCharField implies null==blank==True")
kwargs['null'] = kwargs['blank'] = True
super(NullCharField, self).__init__(*args, **kwargs)
def to_python(self, value):
val = super(NullCharField, self).to_python(value)
return val if val is not None else u''
def get_prep_value(self, value):
prepped = super(NullCharField, self).get_prep_value(value)
return prepped if prepped != u"" else None
def deconstruct(self):
"""
deconstruct() is needed by Django's migration framework
"""
name, path, args, kwargs = super(NullCharField, self).deconstruct()
del kwargs['null']
del kwargs['blank']
return name, path, args, kwargs
大体あってた.
__init__でnull, blankのフラグチェックをしてた.
あと,migration用にdeconstructのoverwriteも必要らしい.