DataMapper を使う (Valication)

http://d.hatena.ne.jp/KrdLab/20090503/1241331627 の補足.

今回は Validation についてです.

一部のサンプルは公式サイトから拝借しています.
http://datamapper.org/doku.php?id=docs:validations

はじめに

Validation を使うには,まず require する.

require 'dm-validations'

Validation 指定

  • validates_present
    • 空でないかどうか
  • validates_absent
    • 空かどうか
  • validates_is_accepted
  • validates_is_confirmed
  • validates_format
    • 指定されたフォーマットに従うかどうか
  • validates_length
    • 指定された条件の長さを満たすか
  • validates_with_method
    • validation 用のメソッドを指定する
  • validates_with_block
    • validation 用のブロックを指定する
  • validates_is_number
    • 数値,もしくは数値として解釈できるかどうか
  • validates_is_unique
    • ユニークな値かどうか
  • validates_within

doc: http://datamapper.rubyforge.org/dm-more/


使うときは,以下のように指定する (Manual).

validates_length :name
validates_length [:name, :description]  # 複数の場合


プロパティ定義にそのまま記述することもできる (Auto).
ショートカットのような位置づけ.

  # 暗黙的に validates_present を生成する
  :nullable => false
 
  # 暗黙的に validates_length を生成する
  :length => 20
  :length => (1..20) # cant be null
  :length => (0..20) # can be null
  # :size is a synonym to :length
 
  # 暗黙的に validates_format を生成する
  :format => :email_address   # e-mail 形式の場合はこれを指定すればよい
  :format => /\w+_\w+/
  :format => lambda {|str| str }
  :format => Proc.new { |str| str }


Manual と Auto の例は以下の通り.

require 'dm-validations'

class Account
  include DataMapper::Resource

  property :name,  String

  # manual validation
  validates_length :name, :max => 20

  # auto-validation
  property :content, Text, :length => (100..500)
end

ちなみに,このコードで validation に引っかかる使い方をしても,エラーメッセージは表示されない (明示的に取得する必要がある).
当然,DB にデータも格納されないまま終了する.

Validating

デフォルトでは save/create/update したときに validation される.
任意のタイミングで validation したいときは,valid? メソッドを使用する.
true を返せば有効,false の場合は無効であるとわかる.


all_valid? メソッドを使うと,自身だけでなく,関連するオブジェクトを再帰的に調べ,1 つでも valid? が false を返せば all_valid? も false を返す.

Working with Validation Errors

validator がエラーを発見すると,ValidationErrors が作成される.
これは errors メソッドを通して取得できる.

class Dummy2
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :value, String

  validates_length :name, :max => 10
end

d = Dummy2.new
d.name = '1234567890a'

if d.save then
  p 'success'
else
  d.errors.each do |e|
    p e
  end
end

ってやると,こんな感じに出る.
["Name must be less than 10 characters long"]

Error Messages

:message オプションを指定することで,エラーメッセージを指定することができる.

  validates_is_unique :title, :scope => :section_id,
    :message => "There's already a page of that title in this section"

上の例では,同じ section_id を持つ場合に限り,:title の unique を検証する.
検証に失敗すれば :message で指定した内容をエラーメッセージとして出力する.

例えば,以下のような動作になる.

class Tuple
  include DataMapper::Resource
  property :id,   Serial
  property :key,  String
  property :value,  String
  validates_is_unique :key, :scope => :value, :message => "There's already (key, value)."
end

t = Tuple.new(:key => 'aaa', :value => '111')
t.errors.each {|e| p e } if not t.save

t = Tuple.new(:key => 'aaa', :value => '111')
t.errors.each {|e| p e } if not t.save    # ここで引っかかる

t = Tuple.new(:key => 'aaa', :value => '222')
t.errors.each {|e| p e } if not t.save

t = Tuple.new(:key => 'bbb', :value => '111')
t.errors.each {|e| p e } if not t.save
+----+------+-------+
| id | key  | value |
+----+------+-------+
|  1 | aaa  | 111   |
|  2 | aaa  | 222   |
|  3 | bbb  | 111   |
+----+------+-------+


:messages を指定することで,複数のメッセージを設定できる.

  property :email, String, :nullable => false, :unique => true, :format => :email_address,
                           :messages => {
                             :presence => "We need your email address.",  # nullable に引っかかったとき
                             :is_unique => "We already have that email.", # unique に引っかかったとき
                             :format => "Doesn't look like an email address to me ..."  # format に引っかかったとき
                           }


TODO まだ調べてない => @resource.errors.add(:title, "Doesn't mention DataMapper")

Custom Validations

validates_with_block や validates_with_method を用いることで,validation の内容を定義することができる.
有効な場合は true を,無効な場合は false を返す.
false の場合に [false, "エラーメッセージ"] の様に返すこともできる.

class Custom
  include DataMapper::Resource
  property :id,   Serial
  property :title,String
  property :body, Text

  validates_with_method :check_body

  def check_body
    if self.body.index('krdlab').nil? then
      return [false, "Not contains 'krdlab'"]
    else
      return true
    end
  end
end

c = Custom.new(:title => 'TITLE', :body => "krdlab's dialy")
c.errors.each {|e| p e } if not c.save

c = Custom.new(:title => 'TITLE', :body => "hoge's dialy")
c.errors.each {|e| p e } if not c.save    # ここで引っかかる
["Not contains 'krdlab'"]

save したり,valid? を呼ぶと実行される.
個々のプロパティに設定することもできる.

validates_with_method :check_body

validates_with_method :body, :method => :check_body

とする.

Conditional Validations

validations は常に走らせなければならないわけではない.
例えば,新規作成時のレコードと,更新時のレコードでは,チェックする内容が異なるかもしれない.

このような,条件に応じて検証を行わせたい場合は,:if や :unless を用いる.

:if や :unless には,メソッド名か Proc を指定する.
メソッドや Proc が true を返すと,validation を実行する.
メソッドの場合は引数なし,Proc の場合は検証しようとしている Resource そのものが引数として渡る.

class Ticket
  include DataMapper::Resource

  property :id, Serial
  property :title, String, :nullable => false
  property :description, Text
  property :commit, String
  property :status, Enum[:new, :open, :invalid, :complete]

  validates_present :commit, :if => Proc.new {|t| t.status == :complete }
end

:title は常に検証される.
:commit は :status が :complete の場合に検証される.

:change_summary に対して,以下のような検証条件を設定することもできる.

validates_length :change_summary, :min => 10, :unless => :new_record?

新規レコードでない場合は,10 文字未満のサマリを認めない,という意味になる.

Contextual Validations

適用する validation のグループを作成し,valid? や save に指定することができる.

サンプルをみた方がわかりやすい.

以下の例では draft 版と publish 版とで,validation の内容を変えている.

  class Article
    include DataMapper::Resource
 
    property :id, Serial
    property :title, String
    property :sidebar_picture_url, String
    property :body, Text
    property :published, Boolean
 
    # validations
    validates_present :title,                 :when => [:draft, :publish]
    validates_present :sidebar_picture_url,   :when => [        :publish]
    validates_present :body,                  :when => [:draft, :publish]
    validates_length :body, :minimum => 1000, :when => [        :publish]
    validates_absent :published,              :when => [:draft]
  end
 
  article = Article.new

  # 呼び出し方法の紹介 ----

  article.valid?(:draft)
  #=> false
  # title と body がない.
 
  article.valid_for_publish?
  #=> false
  # title, sidevar_picture_url, body がない.
  # ちなみに valid_for_publish? は valid?(:publish) と等価.


  # draft は通るよ ----
  article.title = "DataMapper is awesome because ..."
  article.body = "Well, where to begin ..."

  article.valid?(:draft)
  #=> true
  # title と body があるので OK
 
  article.valid?(:publish)
  #=> false
  # body が短すぎる.

  article.save(:draft)
  #=> true


  # draft/publish 両方通るよ ----
  article.sidebar_picture_url = "http://www.greatpictures.com/flower.jpg"
  article.body = "1000 文字以上の文章..."
 
  article.valid?(:draft)
  #=> true.
 
  article.valid?(:publish)
  #=> true.
  # :publish の内容を満たしたので.


  # publish は通るよ ----
  article.published = true

  article.save(:draft)
  #=> false
  # published が true なので.
 
  article.save(:publish)
  #=> true

Setting Properties Before Validation

validation が適用される前に,プロパティに対して値を設定することができる.


デフォルト値設定は参照されるプロパティ自身の値を設定するのに対し,ここで紹介する方法は参照されるプロパティ以外のプロパティに対して値を設定することができる.


以下の例では,valid? メソッドを hook し,permalink に値を設定している.

  class Article
    include DataMapper::Resource
 
    property :id, Serial
    property :title, String, :nullable => false
    property :permalink, String, :nullable => false

    # 詳細は http://datamapper.org/doku.php?id=docs:hooks
    before :valid?, :set_permalink
 
    def set_permalink(context = :default)
      self.permalink = title.gsub(/\s+/,'-')
    end
  end

こんな感じになる.

a = Article2.new
a.title = 'This is a title'
a.save
+----+-----------------+-----------------+
| id | title           | permalink       |
+----+-----------------+-----------------+
|  1 | This is a title | This-is-a-title |
+----+-----------------+-----------------+