ActiveRecord - 资料验证及回呼

I’m not a great programmer; I’m just a good programmer with great habits. - Kent Beck

这一章我们探讨一个ActiveRecord Model的生命周期,包括了储存时的资料验证,以及不同阶段的回呼机制。

Validation 资料验证

ActiveRecord 的 Validation 验证功能,透过 Rails 提供的方法,你可以设定资料的规则来检查资料的正确性。如果验证失败,就无法存进数据库。

那在什么时候会触发这个行为呢?Rails在储存的时候,会自动呼叫model物件的valid?方法进行验证,并将验证结果放在errors里面。所以我们前一章可以看到

  1. > e = Event.new
  2. > e.errors # 目前还是空的
  3. > e.errors.empty? # true
  4. > e.valid? # 进行验证
  5. > e.errors.empty? # false
  6. > e.errors.full_messages # 拿到所有验证失败的讯息

和 Database integrity 不同,这里是在应用层设计验证功能,好处是撰写程式非常容易,Rails 已经整合进 HTML 表单的使用者接口。但是如果你的数据库不只有 Rails 读取,那你除了靠 ActiveRecord 之外,也必须要 DB 层实作 integrity 才能确保资料的正确性。

确保必填

validates_presence_of 是最常用的规则,用来检查资料为非 nil 或空字串。

  1. class Person < ApplicationRecord
  2. validates_presence_of :name
  3. validates_presence_of :login
  4. validates_presence_of :email
  5. end

你也可以合并成一行

  1. class Person < ApplicationRecord
  2. validates_presence_of :name, :login, :email
  3. end

确保字串长度

validates_length_of 会检查字串的长度

  1. class Person < ApplicationRecord
  2. validates_length_of :name, :minimum => 2 # 最少 2
  3. validates_length_of :bio, :maximum => 500 # 最多 500
  4. validates_length_of :password, :in => 6..20 # 介于 6~20
  5. validates_length_of :registration_number, :is => 6 # 刚好 6
  6. end

确保数字

validates_numericality_of 会检查必须是一个数字,以及数字的大小

  1. class Player < ApplicationRecord
  2. validates_numericality_of :points
  3. validates_numericality_of :games_played, :only_integer => true # 必须是整数
  4. validates_numericality_of :age, :greater_than => 18
  5. end

除了 greater_than,还有 greater_than_or_equal_to, equal_to, less_than, less_than_or_equal_to 等参数可以使用。

确保唯一

检查资料在资料表中必须唯一。:scope 参数可以设定范围,例如底下的 :scope => :year 表示,在 Holiday 资料表中,相同 year 的 name 必须唯一。

  1. class Account < ApplicationRecord
  2. validates_uniqueness_of :email
  3. end
  4. class Holiday < ApplicationRecord
  5. validates_uniqueness_of :name, :scope => :year
  6. end

另外还有个参数是 :case_sensitive 默认是 true,表示要区分大小写。

这条规则并没有办法百分百确定唯一,如果很接近的时间内有多个 Rails processes 一起更新数据库,就有可能发生重复的情况。比较保险的作法是数据库也要设定唯一性。

确保格式正确

透过正规表示法检查资料的格式是否正确,例如可以用来检查 Email、URL 网址、邮递区号、手机号码等等格式的正确性。

  1. class User < ApplicationRecord
  2. validates_format_of :email, :with => /\A([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i
  3. validates_format_of :url, :with => /(^$)|(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix
  4. end

正规表示法(regular expression)是一种用来比较字串非常有效率的方式,读者可以利用 Rubular 进行练习。

确保资料只能是某些值

用来检查资料必须只能某些值,例如以下的 status 只能是 pending 或 sent。

  1. class Message < ApplicationRecord
  2. validates_inclusion_of :status, :in => ["pending", "sent"]
  3. end

另外还有较少用到的 validates_exclusion_of 则是确保资料一定不会是某些值。

可共享的验证参数

以下这些参数都可以用在套用在上述的验证方法上:

allow_nil

允许资料是 nil。也就是如果资料是 nil,那就略过这个检查。

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

allow_blank

允许资料是 nil 或空字串。

  1. class Topic < ApplicationRecord
  2. validates_length_of :title, :is => 5, :allow_blank => true
  3. end
  4. Topic.create("title" => "").valid? # => true
  5. Topic.create("title" => nil).valid? # => true

message

设定验证错误时的讯息,若没有提供则会用 Rails 内建的讯息。

  1. class Account < ApplicationRecord
  2. validates_uniqueness_of :email, :message => "你的 Email 重复了"
  3. end

on

可以设定只有新建立(:create)或只有更新时(:update)才验证。默认值是都要检查(:save)。

  1. class Account < ApplicationRecord
  2. validates_uniqueness_of :email, :on => :create
  3. end

if, unless

可以设定只有某些条件下才进行验证

  1. class Event < ApplicationRecord
  2. validates_presence_of :description, :if => :normal_user?
  3. def normal_user?
  4. !self.user.try(:admin?)
  5. end
  6. end

整合写法

在 Rails3 之后也可以用以下的整合写法:

  1. validates :name, :presence => true,
  2. :length => {:minimum => 1, :maximum => 254}
  3. validates :email, :presence => true,
  4. :length => {:minimum => 3, :maximum => 254},
  5. :uniqueness => true,
  6. :email => true

如果需要客制化错误讯息的话:

  1. validates :name, :presence => { :message => "不能空白" } ,
  2. :length => {:minimum => 1, :maximum => 254, :message => "长度不正确" }

如何自定 validation?

使用 validate 方法传入一个同名方法的 Symbol 即可。

  1. validate :my_validation
  2. private
  3. def my_validation
  4. if name =~ /foo/
  5. errors.add(:name, "can not be foo")
  6. elsif name =~ /bar/
  7. errors.add(:name, "can not be bar")
  8. elsif name == 'xxx'
  9. errors.add(:base, "can not be xxx")
  10. end
  11. end

在你的验证方法之中,你会使用到 errors 来将错误讯息放进去,如果这个错误是因为某一属性造成,我们就用那个属性当做 errors 的 key,例如本例的 :name。如果原因不特别属于某一个属性,照惯例会用 :base

数据库层级的验证

在本章开头就有提到,Rails的验证只是在应用层输入资料时做检查,没有办法保证数据库里面的资料一定是正确的。如果您想要在这方面严谨一些,可以在migration新增字段时,加上:null => false确保有值、:limit参数限制长度,或是透过Unique Key确保唯一性,例如:

  1. create_table :registrations do |t|
  2. t.string :name, :null => false, :limit => 100
  3. t.integer :serial
  4. t.timestamps
  5. end
  6. add_index :registrations, :serial, :unique => true

这样数据库就会确认name必须有值(不能是NULL),长度不能大于100 bytesserial是唯一的。

忽略资料验证

透过:validate参数我们可以在save时忽略资料验证:

  1. > event.save( :validate => false )

如果透过update_column更新特定字段的值,也会忽略资料验证:

  1. > event.update_column( :name , nil )

回呼 Callback

在介绍过验证之后,接下来让我们来看看回呼。回呼可以在Model资料的生命周期,挂载事件上去,例如我们可以在资料储存进数据库前,做一些修正,或是再储存成功之后,做一些其他动作。回呼大致可以分成三类:

  • Validation验证前后
  • 在储存进数据库前后
  • 在从数据库移除前后以下是当一个物件储存时的流程,其中1~7就是回呼可以触发的时机:
  • (-) save
  • (-) valid
  • (1) before_validation
  • (-) validate
  • (2) after_validation
  • (3) before_save
  • (4) before_create
  • (-) create
  • (5) after_create
  • (6) after_save
  • (7) after_commit

来看几个使用情境范例

before_validation

常用来清理资料或设定默认值:

  1. class Event < ApplicationRecord
  2. before_validation :setup_defaults
  3. protected
  4. def setup_defaults
  5. self.name.try(:strip!) # 把前后空白去除
  6. self.description = self.name if self.description.blank?
  7. self.is_public ||= true
  8. end
  9. end

after_save

常用来触发去相关联资料的方法或资料:

  1. class Attendee < ApplicationRecord
  2. belongs_to :event
  3. after_save :check_event_status!
  4. protected
  5. def check_event_status!
  6. self.event.check_event_status!
  7. end end

after_destroy

可以用来清理干净相关联的资料

其他注意事项

  • 回呼的方法一般会放在protectedprivate下,确保从Model外部是无法呼叫的。
  • beforevalidationbefore_save的差别在于后者不会经过_Validation资料验证。
  • 在回呼中如果想要中断,可以用 throws :abort。在 Rails 5 之前的版本则是用 return false (这是一个很难 debug 的坑,因为你可能不小心 return false 而不自知)

其中afterrollbackafter_commit这两个回呼和_Transaction交易有关。Rollback指的是在transaction区块中发生例外时,Rails会将原先transaction中已经被执行的所有资料操作回复到执行transaction前的状态,afterrollback就是让你在_rollback完成时所触发的回呼,而aftercommit是指在_transaction完成后才触发的回呼,如果要触发异步的操作,会需要放在after_commit之中,才能确保异步的Process进程能够拿到刚刚才存进去的资料。关于transaction的部份请参考ActiveRecord进阶功能一章的交易一节。

更多线上资源