GAE のデータストアでは、値をプロパティにセットした時点で、validate されてしまう為、次のような問題があります(少なくとも僕にとっては)。
- 複数の属性に関係するバリデーションが実現できない。
- 入力フォームの表示用等で空のインスタンスが欲しい時、インスタンス化された時点で validate されるので利用できない。
これらの回避の為に、バリデーション周りを以下のようにして変えてみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
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 クラス
エラー情報を処理するコンテナ。
以下のように利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 の今後の変更にもやや強いかと思っています。
…次はバリデーション エラーメッセージの日本語化ですね…。