GAE の Model の バリデーションの動作を変えてみた
GAE のデータストアでは、値をプロパティにセットした時点で、
validate されてしまう為、次のような問題があります(少なくとも僕にとっては)。
これらの回避の為に、バリデーション周りを以下のようにして変えてみました。
from google.appengine.ext import db #db.Propertyを変更して、規定のバリデーションをスキップする setattr(db.Property, '__set__', lambda self, model_instance, value: setattr(model_instance, self._attr_name(), value)) #バリデーション機能を追加したベースモデル class BaseModel(db.Model): errors = None def __init__(self, *args, **kwds): super(BaseModel, self).__init__(*args, **kwds) self.errors = Errors() def validate(self): for prop in self.properties().values(): value = getattr(self, prop._attr_name(), None) try: value = prop.validate(value) setattr(self, prop._attr_name(), value) except db.BadValueError, e: self.errors.append(prop, e.message) return not bool(self.errors) is_valid = property(lambda self: self.validate()) #バリデーションエラーを保存するコンテナ class Error(object): def __init__(self, prop, msg=None): self.prop = prop self.msg = msg def __str__(self): return self.tostr() def tostr(self, format=u"[%s] %s"): if isinstance(self.prop, db.Property) and format: return format % (self.prop.verbose_name or self.prop.name, self.msg) else: return self.msg #エラーの集合を扱うコンテナ class Errors(list): def __init__(self): self.map = dict() def append(self, prop, msg=None, error=True): if error: if isinstance(prop, db.Property): super(Errors, self).append(Error(prop, msg)) self.map[prop.name] = len(self) - 1 elif isinstance(prop, (str, unicode)): super(Errors, self).append(Error(prop, msg)) self.map[prop] = len(self) - 1 else: super(Errors, self).append(Error(None, prop)) def clear(self): del self[:] def get(self, name, default=None): return self[name] if name in self else default def tostr(self, sep=u"\n", **ops): msgs=list() for error in self: msgs.append(error.tostr(**ops)) return sep.join(msgs) def __str__(self): return self.tostr() def __contains__(self, name): return name in self.map def __getitem__(self, index): if isinstance(index, (str, unicode)): if index not in self.map: raise IndexError(u"'%s' property has not error." % index) index = self.map[index] return super(Errors, self).__getitem__(index)
以下、簡単な解説です。
- setattr(db.Property, '__set__',……
- 今回のキモ。db.Property クラスの __set__ デスクリプタ メソッドを上書きし、属性代入時のバリデーションをバイパスしています。GAE v1.2.6 において、Property の定義済み実装クラス群では、ReferenceProperty 以外は __set__ のオーバーライドがありません。ReferenceProperty は __set__ がオーバーライドされていますが、目的に対しては特に影響無いものでしたので、属性保存時のバリデーションの無効化は、とりあえずこれで OK!
- BaseModel クラス
- GAE の db.Model をサブクラスを作成し validate メソッドを追加しています。validate メソッドは、Model インスタンスの全ての属性をバリデーションし、検出されたエラーを、errors に保存します。アプリケーションの Model は、この BaseModel を継承して作成し、保存の前には必ず、validate()(または is_valid)を呼んでください。
- Errors/Error クラス
- エラー情報を処理するコンテナ。
以下のように利用します。
class Client(BaseModel): name = db.StringProperty(u"お名前", required=True) email = db.EmailProperty(u"E mail", required=True) phone = db.PhoneNumberProperty(u"お電話") client = Client(name=u"ぷりっけ。") #email がなくてもインスタンスを生成可 if client.is_valid: #is_valid または validate() で全属性をバリデーション client.put() #OKならデータストアへ保存 else: error_message = client.errors.tostr() #エラーをまとめて取得できる #また、エラーは以下のように検査、取得、追加できます。 'name' in client.errors #属性のエラーの有無を確認 client.errors['name'] #属性の Error が在れば返す。無ければ IndexError client.errors['name'].tostr() #属性のエラーメッセージを取得 client.errors.tostr() #全てのエラーメッセージを取得 client.errors.append('body','[内容] が未記入です。') #エラーを追加 client.errors.append('body','[内容] が未記入です。', not body) #特定の場合のみ
…とまあ、こんな感じです。
注意点として、Rails のように、put() などで自動的に validate はされません。必ず、is_valid の参照、または validate() の呼び出しが必要です。実装するのは簡単ですが、これぐらいで不便は無いと思うし、GAE 側の Model の今後の変更にもやや強いかと思っています。
…次はバリデーション エラーメッセージの日本語化やね…。
以上っす。
★来ていただいて(人'▽`)ありがとう☆
土俵は違うけど、この情報が役に立った人は、クリックしてね!
もしよければ応援のクリックお願いしまーす♪