1.對象生命周期
通常情況下,在rails應用中,對象會被創建,修改和刪除。ActiveRecord針對這些對象提供了攔截,你可以控制你的應用和這些對象。
驗證保證了存入數據庫的數據都是有效的。回調和觀察者允許你在對象狀態發生變化的前後進行一些邏輯操作。
2.驗證
2.1.為什麼需要驗證
驗證保證了只有合法的數據才可以存入數據庫。例如,你的應用需要確保每個用戶都擁有合法的電子郵件地址和郵寄地址。
在存入數據庫之前,有很多方法可以驗證數據的合法性。包括數據庫約束,客戶端的驗證,controller級別的驗證,model級別的驗證。
數據庫約束或者是存儲過程中的驗證是依賴於數據庫的,難以維護和測試。如果你的數據庫還會被其他應用使用,那麼在數據庫級別的約束是個好主意。另外,數據庫級別的驗證可以安全的實現一些事情,例如唯一性約束,這樣的需求在應用中實現相對較難。
客戶端驗證是很有用的,但是不能單獨的信任客戶端驗證。如果使用javascript實現,是可以繞過的。但是,結合其他技術,客戶端驗證可以在用戶訪問你的網站的時候,給用戶很快的反饋。
controller級別的驗證也是可以的,但是通常它們很笨重,難以測試和維護。無論什麼時候,保持controller的精簡都是一個好主意,使得你的應用長期保持一個好的工作。
model級別的驗證是保證合法數據存入數據庫的最好方法。它們是數據庫無關的,不能被終端用戶繞過,很方便測試和維護。在rails中,很容易使用,提供了內置的輔助方法,還可以創建自己的驗證方法。
2.2.驗證在什麼時候發生
有兩種類型的ActiveRecord對象:一種對應於數據庫的一行數據,另一種不是。在你創建一個新的對象,就是調用new方法之後,數據庫中還不存在這條記錄。一旦你調用了save方法,就會存入數據庫。可以使用new_record?實例方法來判斷對象是否存在於數據庫。
class Person < ActiveRecord::Base end p = Person.new(:name => "shi") p.new_record? #=> false p.save #=> true p.new_record? #=> true
create並調用save方法會給數據庫發送insert語句,更新一個已經存在的記錄就是給數據庫發送update語句。驗證發生在發送這些語句之前。如果驗證失敗,對象被標記為非法,不會發送insert或update語句,這就避免了非法數據存入數據庫。你可以在created,saved和updated的時候執行指定的驗證規則。
下面的方法將會觸發驗證,如果對象是合法的,就會存入數據庫。
create
create!
save
save!
update
update_attributes
update_attributes!
有歎號的方法在對象是非法的時候會拋出異常。沒有歎號的save和update_attributes在非法的時候返回false,create和update返回對象。
2.3.跳過驗證
下面的方法會跳過驗證,不管是否合法都會存入數據庫,使用的時候要小心。
decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_counters
save方法通過添加:validate => false參數也可以跳過驗證,要小心使用。
p.save(:validate => false)
2.4.Valid? and Invalid?
通過valid?方法來判斷對象是否合法,會觸發在model中定義的validates,如果沒有錯誤,返回true,否則返回false代表不合法。
class Person < ActiveRecord::Base validates :name, :presence => true end Person.create(:name => "John Doe").valid? # => true Person.create(:name => nil).valid? # => false
ActiveRecord執行驗證之後,對象的errors會返回一個集合。如果是合法的,這個集合就是空的。
new之後創建的對象,即使是不合法的,errors集合也是空的,因為在new的時候還沒有觸發驗證。
class Person < ActiveRecord::Base validates :name, :presence => true end >> p = Person.new => #<Person id: nil, name: nil> >> p.errors => {} >> p.valid? => false >> p.errors => {:name=>["can't be blank"]} >> p = Person.create => #<Person id: nil, name: nil> >> p.errors => {:name=>["can't be blank"]} >> p.save => false >> p.save! => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank >> Person.create! => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
invalid?和valid?方法相反,它也會觸發驗證,如果有erros就返回true,否則返回false。
2.5.errors[]
通過errors[:attribute]可以驗證在某個指定的屬性是否合法,返回的是一個數組,如果這個屬性沒有問題,返回的數組是空的。
這個方法只在驗證發生之後才有用,因為它只是檢查errors集合,並不觸發驗證,只是檢查在某一個指定的屬性上是否有errors。
class Person < ActiveRecord::Base validates :name, :presence => true end >> Person.new.errors[:name].any? # => false >> Person.create.errors[:name].any? # => true
3.驗證輔助工具
ActiveRecord預先定義了很多的驗證工具,你可以直接使用它們。定義了常見的驗證規則,每次驗證失敗,都會在errors集合中添加對象,信息和驗證的屬性相關。
每個驗證方法都會接受任意屬性的名稱,因此你可以在一條驗證語句中添加多個屬性。
每個驗證都接受:on和:message的可選項,定義驗證在什麼時候觸發,在驗證失敗之後需要在errors中添加什麼信息。:on選項的值可以是:save, :create, :update中的一個,:save是默認值。每個驗證方法都有默認的提示信息。下面列出一些常見的驗證方法。
3.1.acceptance
驗證在提交的表單中,用戶是否勾選了checkbox。常見的場景包括:用戶同意服務條款,確定閱讀了一段文本,等類似場景。這種驗證在web應用中很常用,通常不需要存入數據庫(除非你的數據庫有對應的列)。如果不需要存儲,那麼只是一個虛擬的屬性。
class Person < ActiveRecord::Base validates :terms_of_service, :acceptance => true end
默認的錯誤信息是:must be accepted。
class Person < ActiveRecord::Base validates :terms_of_service, :acceptance => { :accept => 'yes' } end
還可以通過:accept來定義可以接受的值。
3.2.validates_associated
用來驗證表關系,當你save對象的時候,驗證每個關聯。
class Library < ActiveRecord::Base has_many :books validates_associated :books end
注意不要在關系的雙方都進行這樣的驗證,會造成死循環驗證。
3.3.confirmation
用來驗證兩個輸入框應該輸入相同的內容,例如驗證郵件和密碼。驗證會創建一個虛擬的屬性,屬性名稱以"_confirmation"結尾。
class Person < ActiveRecord::Base validates :email, :confirmation => true end
在view中你可以使用下面的方式。
<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>
這個驗證只是在email_confirmation不為nil的是才觸發,為了滿足需求,還要在email_confirmation中添加:presence驗證。
class Person < ActiveRecord::Base validates :email, :confirmation => true validates :email_confirmation, :presence => true end
3.4.exclusion
用來驗證一個屬性是否不在指定的集合中。這個集合是一個枚舉對象集合。
class Account < ActiveRecord::Base validates :subdomain, :exclusion => { :in => %w(www us ca jp), :message => "Subdomain %{value} is reserved." } end
:in選項用來指定集合,:in有一個別名:within,你也可以用它實現相同的功能。
3.5.format
用來驗證指定的屬性是否符合正則表達式,:with來指定正則表達式。
class Product < ActiveRecord::Base validates :legacy_code, :format => { :with => /\A[a-zA-Z]+\z/, :message => "Only letters allowed" } end
3.6.inclusion
用來驗證一個屬性是否在指定的集合中。這個集合是一個枚舉對象集合。
class Account < ActiveRecord::Base validates :subdomain, :inclusion => { :in => %w(www us ca jp), :message => "Subdomain %{value} is reserved." } end
:in選項用來指定集合,:in有一個別名:within,你也可以用它實現相同的功能。
3.7.length
用來驗證屬性的長度。
class Person < ActiveRecord::Base validates :name, :length => { :minimum => 2 } validates :bio, :length => { :maximum => 500 } validates :password, :length => { :in => 6..20 } validates :registration_number, :length => { :is => 6 } end
class Person < ActiveRecord::Base validates :bio, :length => { :maximum => 1000, :too_long => "%{count} characters is the maximum allowed" } end class Essay < ActiveRecord::Base validates :content, :length => { :minimum => 300, :maximum => 400, :tokenizer => lambda { |str| str.scan(/\w+/) }, :too_short => "must have at least %{count} words", :too_long => "must have at most %{count} words" } end
size是length的別名,上面的length可以用size代替。
3.8.numericality
驗證屬性只接受數值類型。
:only_integer => true相當於使用
/\A[+-]?\d+\Z/
正則表達式。否則將會嘗試使用Float轉換這個值。
class Player < ActiveRecord::Base validates :points, :numericality => true validates :games_played, :numericality => { :only_integer => true } end
除了:only_integer還接受其他的選項。
:greater_than
:greater_than_or_equal_to
:equal_to
:less_than
:less_than_or_equal_to
:odd
:even
3.9.presence
用來驗證屬性不可空。調用blank?方法檢查字符串是否nil或者空白,空白包括空字符串和空格字符串兩種類型。
class Person < ActiveRecord::Base validates :name, :login, :email, :presence => true end
驗證關聯是必須存在的,只要驗證外鍵就可以。
class LineItem < ActiveRecord::Base belongs_to :order validates :order_id, :presence => true end
驗證boolean類型的字段。
validates :field_name, :inclusion => { :in => [true, false] }.
3.10.uniqueness
驗證屬性的唯一性。它不會在數據庫中創建唯一約束。還是會發生兩個不同的數據庫連接,創建兩個相同值的記錄。所以最好在數據庫創建唯一約束。
class Account < ActiveRecord::Base validates :email, :uniqueness => true end
這個驗證發生在執行sql語句的時候,查詢是否存在相同的記錄。
:scope選項用來限制驗證的范圍。
class Holiday < ActiveRecord::Base validates :name, :uniqueness => { :scope => :year, :message => "should happen once per year" } end
:case_sensitive選項指定是否大小寫敏感。
class Person < ActiveRecord::Base validates :name, :uniqueness => { :case_sensitive => false } end
有些數據庫是可以配置大小寫敏感的。
3.11.validates_with
指定單獨的驗證類。
class Person < ActiveRecord::Base validates_with GoodnessValidator end class GoodnessValidator < ActiveModel::Validator def validate(record) if record.first_name == "Evil" record.errors[:base] << "This person is evil" end end end
指定驗證的字段。
class Person < ActiveRecord::Base validates_with GoodnessValidator, :fields => [:first_name, :last_name] end class GoodnessValidator < ActiveModel::Validator def validate(record) if options[:fields].any?{|field| record.send(field) == "Evil" } record.errors[:base] << "This person is evil" end end end
3.12.validates_each
自定義驗證block。
class Person < ActiveRecord::Base validates_each :name, :surname do |record, attr, value| record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/ end end
4.常用的驗證選項
4.1.:allow_nil
class Coffee < ActiveRecord::Base validates :size, :inclusion => { :in => %w(small medium large), :message => "%{value} is not a valid size" }, :allow_nil => true end
4.2.:allow_blank
class Topic < ActiveRecord::Base validates :title, :length => { :is => 5 }, :allow_blank => true end Topic.create("title" => "").valid? # => true Topic.create("title" => nil).valid? # => true
4.3.:message
非法之後的提示消息
4.4.:on
指定驗證發生的時機。默認的時機是save(創建和更新的時候)。
class Person < ActiveRecord::Base # it will be possible to update email with a duplicated value validates :email, :uniqueness => true, :on => :create # it will be possible to create the record with a non-numerical age validates :age, :numericality => true, :on => :update # the default (validates on both create and update) validates :name, :presence => true, :on => :save end
5.條件驗證
class Order < ActiveRecord::Base validates :card_number, :presence => true, :if => :paid_with_card? def paid_with_card? payment_type == "card" end end
class Person < ActiveRecord::Base validates :surname, :presence => true, :if => "name.nil?" end
class Account < ActiveRecord::Base validates :password, :confirmation => true, :unless => Proc.new { |a| a.password.blank? } end
class User < ActiveRecord::Base with_options :if => :is_admin? do |admin| admin.validates :password, :length => { :minimum => 10 } admin.validates :email, :presence => true end end
6.自定義驗證
6.1.自定義驗證類
class MyValidator < ActiveModel::Validator def validate(record) unless record.name.starts_with? 'X' record.errors[:name] << 'Need a name starting with X please!' end end end class Person include ActiveModel::Validations validates_with MyValidator end
class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i record.errors[attribute] << (options[:message] || "is not an email") end end end class Person < ActiveRecord::Base validates :email, :presence => true, :email => true end
6.2.自定義驗證方法
class Invoice < ActiveRecord::Base validate :expiration_date_cannot_be_in_the_past, :discount_cannot_be_greater_than_total_value def expiration_date_cannot_be_in_the_past if !expiration_date.blank? and expiration_date < Date.today errors.add(:expiration_date, "can't be in the past") end end def discount_cannot_be_greater_than_total_value if discount > total_value errors.add(:discount, "can't be greater than total value") end end end
class Invoice < ActiveRecord::Base validate :active_customer, :on => :create def active_customer errors.add(:customer_id, "is not active") unless customer.active? end end
ActiveRecord::Base.class_eval do def self.validates_as_choice(attr_name, n, options={}) validates attr_name, :inclusion => { { :in => 1..n }.merge!(options) } end end
class Movie < ActiveRecord::Base validates_as_choice :rating, 5 end