Rails开发细节《六》ActiveRecord Validationa and Callbacks验证和回调

1.对象生命周期

通常情况下,在rails应用中,对象会被创建,修改和删除。ActiveRecord针对这些对象提供了拦截,你可以控制你的应用和这些对象。

验证保证了存入数据库的数据都是有效的。回调和观察者允许你在对象状态发生变化的前后进行一些逻辑操作。

2.验证

2.1.为什么需要验证

验证保证了只有合法的数据才可以存入数据库。例如,你的应用需要确保每个用户都拥有合法的电子邮件地址和邮寄地址。

在存入数据库之前,有很多方法可以验证数据的合法性。包括数据库约束,客户端的验证,controller级别的验证,model级别的验证。

  • 数据库约束或者是存储过程中的验证是依赖于数据库的,难以维护和测试。如果你的数据库还会被其他应用使用,那么在数据库级别的约束是个好主意。另外,数据库级别的验证可以安全的实现一些事情,例如唯一性约束,这样的需求在应用中实现相对较难。
  • 客户端验证是很有用的,但是不能单独的信任客户端验证。如果使用javascript实现,是可以绕过的。但是,结合其他技术,客户端验证可以在用户访问你的网站的时候,给用户很快的反馈。
  • controller级别的验证也是可以的,但是通常它们很笨重,难以测试和维护。无论什么时候,保持controller的精简都是一个好主意,使得你的应用长期保持一个好的工作。
  • model级别的验证是保证合法数据存入数据库的最好方法。它们是数据库无关的,不能被终端用户绕过,很方便测试和维护。在rails中,很容易使用,提供了内置的辅助方法,还可以创建自己的验证方法。

2.2.验证在什么时候发生

有两种类型的ActiveRecord对象:一种对应于数据库的一行数据,另一种不是。在你创建一个新的对象,就是调用new方法之后,数据库中还不存在这条记录。一旦你调用了save方法,就会存入数据库。可以使用new_record?实例方法来判断对象是否存在于数据库。

 

  1. class Person < ActiveRecord::Base 
  2.  
  3. end 
  4.  
  5. p = Person.new(:name => "shi"
  6. p.new_record?  #=> false 
  7.  
  8. p.save  #=> true 
  9.  
  10. 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参数也可以跳过验证,要小心使用。

 

  1. p.save(:validate => false

2.4.Valid? and Invalid?

通过valid?方法来判断对象是否合法,会触发在model中定义的validates,如果没有错误,返回true,否则返回false代表不合法。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :name:presence => true 
  3. end 
  4.   
  5. Person.create(:name => "John Doe").valid? # => true 
  6. Person.create(:name => nil).valid? # => false 

 

ActiveRecord执行验证之后,对象的errors会返回一个集合。如果是合法的,这个集合就是空的。

new之后创建的对象,即使是不合法的,errors集合也是空的,因为在new的时候还没有触发验证。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :name:presence => true 
  3. end 
  4.   
  5. >> p = Person.new 
  6. => #<Person id: nil, name: nil> 
  7. >> p.errors 
  8. => {} 
  9.   
  10. >> p.valid? 
  11. => false 
  12. >> p.errors 
  13. => {:name=>["can't be blank"]} 
  14.   
  15. >> p = Person.create 
  16. => #<Person id: nil, name: nil> 
  17. >> p.errors 
  18. => {:name=>["can't be blank"]} 
  19.   
  20. >> p.save 
  21. => false 
  22.   
  23. >> p.save! 
  24. => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank 
  25.   
  26. >> Person.create! 
  27. => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank 

invalid?和valid?方法相反,它也会触发验证,如果有erros就返回true,否则返回false。

2.5.errors[]

通过errors[:attribute]可以验证在某个指定的属性是否合法,返回的是一个数组,如果这个属性没有问题,返回的数组是空的。

这个方法只在验证发生之后才有用,因为它只是检查errors集合,并不触发验证,只是检查在某一个指定的属性上是否有errors。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :name:presence => true 
  3. end 
  4.   
  5. >> Person.new.errors[:name].any? # => false 
  6. >> Person.create.errors[:name].any? # => true 

3.验证辅助工具

ActiveRecord预先定义了很多的验证工具,你可以直接使用它们。定义了常见的验证规则,每次验证失败,都会在errors集合中添加对象,信息和验证的属性相关。

每个验证方法都会接受任意属性的名称,因此你可以在一条验证语句中添加多个属性。

每个验证都接受:on和:message的可选项,定义验证在什么时候触发,在验证失败之后需要在errors中添加什么信息。:on选项的值可以是:save, :create, :update中的一个,:save是默认值。每个验证方法都有默认的提示信息。下面列出一些常见的验证方法。

3.1.acceptance

验证在提交的表单中,用户是否勾选了checkbox。常见的场景包括:用户同意服务条款,确定阅读了一段文本,等类似场景。这种验证在web应用中很常用,通常不需要存入数据库(除非你的数据库有对应的列)。如果不需要存储,那么只是一个虚拟的属性。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :terms_of_service:acceptance => true 
  3. end 

默认的错误信息是:must be accepted。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :terms_of_service:acceptance => { :accept => 'yes' } 
  3. end 

还可以通过:accept来定义可以接受的值。

3.2.validates_associated

用来验证表关系,当你save对象的时候,验证每个关联。

 

  1. class Library < ActiveRecord::Base 
  2.   has_many :books 
  3.   validates_associated :books 
  4. end 

注意不要在关系的双方都进行这样的验证,会造成死循环验证。

3.3.confirmation

用来验证两个输入框应该输入相同的内容,例如验证邮件和密码。验证会创建一个虚拟的属性,属性名称以"_confirmation"结尾。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :email:confirmation => true 
  3. end 

在view中你可以使用下面的方式。

 

  1. <%= text_field :person:email %> 
  2. <%= text_field :person:email_confirmation %> 

这个验证只是在email_confirmation不为nil的是才触发,为了满足需求,还要在email_confirmation中添加:presence验证。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :email:confirmation => true 
  3.   validates :email_confirmation:presence => true 
  4. end 

3.4.exclusion

用来验证一个属性是否不在指定的集合中。这个集合是一个枚举对象集合。

 

  1. class Account < ActiveRecord::Base 
  2.   validates :subdomain:exclusion => { :in => %w(www us ca jp), 
  3.     :message => "Subdomain %{value} is reserved." } 
  4. end 

:in选项用来指定集合,:in有一个别名:within,你也可以用它实现相同的功能。

3.5.format

用来验证指定的属性是否符合正则表达式,:with来指定正则表达式。

 

  1. class Product < ActiveRecord::Base 
  2.   validates :legacy_code:format => { :with => /\A[a-zA-Z]+\z/, 
  3.     :message => "Only letters allowed" } 
  4. end 

3.6.inclusion

用来验证一个属性是否在指定的集合中。这个集合是一个枚举对象集合。

 

  1. class Account < ActiveRecord::Base 
  2.   validates :subdomain:inclusion => { :in => %w(www us ca jp), 
  3.     :message => "Subdomain %{value} is reserved." } 
  4. end 

:in选项用来指定集合,:in有一个别名:within,你也可以用它实现相同的功能。

3.7.length

用来验证属性的长度。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :name:length => { :minimum => 2 } 
  3.   validates :bio:length => { :maximum => 500 } 
  4.   validates :password:length => { :in => 6..20 } 
  5.   validates :registration_number:length => { :is => 6 } 
  6. end 

 

  1. class Person < ActiveRecord::Base 
  2.   validates :bio:length => { :maximum => 1000, 
  3.     :too_long => "%{count} characters is the maximum allowed" } 
  4. end 
  5.  
  6.  
  7. class Essay < ActiveRecord::Base 
  8.   validates :content:length => { 
  9.     :minimum   => 300, 
  10.     :maximum   => 400, 
  11.     :tokenizer => lambda { |str| str.scan(/\w+/) }, 
  12.     :too_short => "must have at least %{count} words"
  13.     :too_long  => "must have at most %{count} words" 
  14.   } 
  15. end 

size是length的别名,上面的length可以用size代替。

3.8.numericality

验证属性只接受数值类型。

:only_integer => true相当于使用

 

  1. /\A[+-]?\d+\Z/ 

正则表达式。否则将会尝试使用Float转换这个值。

 

  1. class Player < ActiveRecord::Base 
  2.   validates :points:numericality => true 
  3.   validates :games_played:numericality => { :only_integer => true } 
  4. 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或者空白,空白包括空字符串和空格字符串两种类型。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :name:login:email:presence => true 
  3. end 

验证关联是必须存在的,只要验证外键就可以。

 

  1. class LineItem < ActiveRecord::Base 
  2.   belongs_to :order 
  3.   validates :order_id:presence => true 
  4. end 

验证boolean类型的字段。

 

  1. validates :field_name:inclusion => { :in => [truefalse] }. 

3.10.uniqueness

验证属性的唯一性。它不会在数据库中创建唯一约束。还是会发生两个不同的数据库连接,创建两个相同值的记录。所以最好在数据库创建唯一约束。

 

  1. class Account < ActiveRecord::Base 
  2.   validates :email:uniqueness => true 
  3. end 

这个验证发生在执行sql语句的时候,查询是否存在相同的记录。

:scope选项用来限制验证的范围。

 

  1. class Holiday < ActiveRecord::Base 
  2.   validates :name:uniqueness => { :scope => :year
  3.     :message => "should happen once per year" } 
  4. end 

:case_sensitive选项指定是否大小写敏感。

 

  1. class Person < ActiveRecord::Base 
  2.   validates :name:uniqueness => { :case_sensitive => false } 
  3. end 

有些数据库是可以配置大小写敏感的。

3.11.validates_with

指定单独的验证类。

 

  1. class Person < ActiveRecord::Base 
  2.   validates_with GoodnessValidator 
  3. end 
  4.   
  5. class GoodnessValidator < ActiveModel::Validator 
  6.   def validate(record) 
  7.     if record.first_name == "Evil" 
  8.       record.errors[:base] << "This person is evil" 
  9.     end 
  10.   end 
  11. end 

 

指定验证的字段。

  1. class Person < ActiveRecord::Base 
  2.   validates_with GoodnessValidator, :fields => [:first_name:last_name
  3. end 
  4.   
  5. class GoodnessValidator < ActiveModel::Validator 
  6.   def validate(record) 
  7.     if options[:fields].any?{|field| record.send(field) == "Evil" } 
  8.       record.errors[:base] << "This person is evil" 
  9.     end 
  10.   end 
  11. end 

3.12.validates_each

自定义验证block。

  1. class Person < ActiveRecord::Base 
  2.   validates_each :name:surname do |record, attr, value| 
  3.     record.errors.add(attr, 'must start with upper case'if value =~ /\A[a-z]/ 
  4.   end 
  5. end 

4.常用的验证选项

4.1.:allow_nil

 

  1. class Coffee < ActiveRecord::Base 
  2.   validates :size:inclusion => { :in => %w(small medium large), 
  3.     :message => "%{value} is not a valid size" }, :allow_nil => true 
  4. end 

4.2.:allow_blank

 

  1. class Topic < ActiveRecord::Base 
  2.   validates :title:length => { :is => 5 }, :allow_blank => true 
  3. end 
  4.   
  5. Topic.create("title" => "").valid?  # => true 
  6. Topic.create("title" => nil).valid? # => true 

4.3.:message

非法之后的提示消息

4.4.:on

指定验证发生的时机。默认的时机是save(创建和更新的时候)。

 

  1. class Person < ActiveRecord::Base 
  2.   # it will be possible to update email with a duplicated value 
  3.   validates :email:uniqueness => true:on => :create 
  4.   
  5.   # it will be possible to create the record with a non-numerical age 
  6.   validates :age:numericality => true:on => :update 
  7.   
  8.   # the default (validates on both create and update) 
  9.   validates :name:presence => true:on => :save 
  10. end 

5.条件验证

 

  1. class Order < ActiveRecord::Base 
  2.   validates :card_number:presence => true:if => :paid_with_card
  3.   
  4.   def paid_with_card? 
  5.     payment_type == "card" 
  6.   end 
  7. end 

 

  1. class Person < ActiveRecord::Base 
  2.   validates :surname:presence => true:if => "name.nil?" 
  3. end 

 

  1. class Account < ActiveRecord::Base 
  2.   validates :password:confirmation => true
  3.     :unless => Proc.new { |a| a.password.blank? } 
  4. end 

 

  1. class User < ActiveRecord::Base 
  2.   with_options :if => :is_admindo |admin| 
  3.     admin.validates :password:length => { :minimum => 10 } 
  4.     admin.validates :email:presence => true 
  5.   end 
  6. end 

6.自定义验证

6.1.自定义验证类

 

  1. class MyValidator < ActiveModel::Validator 
  2.   def validate(record) 
  3.     unless record.name.starts_with? 'X' 
  4.       record.errors[:name] << 'Need a name starting with X please!' 
  5.     end 
  6.   end 
  7. end 
  8.   
  9. class Person 
  10.   include ActiveModel::Validations 
  11.   validates_with MyValidator 
  12. end 

 

  1. class EmailValidator < ActiveModel::EachValidator 
  2.   def validate_each(record, attribute, value) 
  3.     unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 
  4.       record.errors[attribute] << (options[:message] || "is not an email"
  5.     end 
  6.   end 
  7. end 
  8.   
  9. class Person < ActiveRecord::Base 
  10.   validates :email:presence => true:email => true 
  11. end 

6.2.自定义验证方法

 

  1. class Invoice < ActiveRecord::Base 
  2.   validate :expiration_date_cannot_be_in_the_past
  3.     :discount_cannot_be_greater_than_total_value 
  4.   
  5.   def expiration_date_cannot_be_in_the_past 
  6.     if !expiration_date.blank? and expiration_date < Date.today 
  7.       errors.add(:expiration_date"can't be in the past"
  8.     end 
  9.   end 
  10.   
  11.   def discount_cannot_be_greater_than_total_value 
  12.     if discount > total_value 
  13.       errors.add(:discount"can't be greater than total value"
  14.     end 
  15.   end 
  16. end 

 

  1. class Invoice < ActiveRecord::Base 
  2.   validate :active_customer:on => :create 
  3.   
  4.   def active_customer 
  5.     errors.add(:customer_id"is not active"unless customer.active? 
  6.   end 
  7. end 

 

  1. ActiveRecord::Base.class_eval do 
  2.   def self.validates_as_choice(attr_name, n, options={}) 
  3.     validates attr_name, :inclusion => { { :in => 1..n }.merge!(options) } 
  4.   end 
  5. end 

 

  1. class Movie < ActiveRecord::Base 
  2.   validates_as_choice :rating, 5 
  3. end 

 

 

 

参考文献

1.Active Record Validations and Callbacks