GAE の Model の バリデーションの動作を変えてみた

GAE のデータストアでは、値をプロパティにセットした時点で、
validate されてしまう為、次のような問題があります(少なくとも僕にとっては)

  1. 複数の属性に関係するバリデーションが実現できない。
  2. 入力フォームの表示用等で空のインスタンスが欲しい時、インスタンス化された時点で 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 の今後の変更にもやや強いかと思っています。


…次はバリデーション エラーメッセージの日本語化やね…。


以上っす。



にほんブログ村 バイクブログ カワサキへ
★来ていただいて(人'▽`)ありがとう☆
土俵は違うけど、この情報が役に立った人は、クリックしてね!
もしよければ応援のクリックお願いしまーす♪