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?实例方法来判断对象是否存在于数据库。
- 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
参考文献