More at rubyonrails.org: Overview | Download | Deploy | Code | Screencasts | Documentation | Ecosystem | Community | Blog

Active Record 的驗證 (Validations) 與回呼 (Callbacks)

這一章說明如何掛載事件(hook)到 Active Record 物件的生命週期 (life cycle):在物件進入資料庫之前,驗證它的狀態,以及在生命週期的特定時間點,執行特訂的操作。

讀完本篇,您會:

Chapters

  1. 物件生命週期 (Object Life Cycle)
  2. 驗證的概觀 (Validations Overview)
  3. 驗證輔助工具 (Validation Helpers)
  4. 共通的驗證選項 (Common Validation Options)
  5. 條件式驗證 (Conditional Validation)
  6. 打造自訂的驗證方法
  7. 處理驗證錯誤
  8. 在視圖中秀出驗證錯誤 (Displaying Validation Errors in the View)
  9. 回呼概觀 (Callbacks Overview)
  10. 可供使用的回呼 (Available Callbacks)
  11. 執行回呼 (Running Callbacks)
  12. 略過回呼 (Skipping Callbacks)
  13. 中止執行鏈 (Halting Execution)
  14. 關連性回呼 (Relational Callbacks)
  15. 條件式回呼 (Conditional Callbacks)
  16. 回呼類別 (Callback Classes)
  17. 觀察者 (Observers)
  18. Transaction Callbacks
  19. 文件修改記錄 (Changelog)
  20. 關於譯者
  21. 翻譯詞彙

1 物件生命週期 (Object Life Cycle)

在 Rails 正常運作期間,會產生一些物件,也會更新、銷毀它們。為了方便您操控應用程式、操控資料,Active Record 提供了 物件生命週期 內的掛載點(hooks)。

其中,驗證 (validations) 的功用,是確保唯有有效的資料,才可以儲存到資料庫裡面。而回呼 (callbacks) 和觀察者 (observers) 的功用,則是在物件狀態變化之前或之後,觸發特定的邏輯。

2 驗證的概觀 (Validations Overview)

深入 Rails 驗證的細節之前,讓我們先了解一下驗證所扮演的角色。

2.1 為什麼要驗證?

驗證的作用,是確保資料庫裡,只有有效資料 (valid data) 才會儲存進去。比方說,您的應用程式可能要確定每個使用者,都提供有效的電子郵件地址、和郵寄地址等。

資料存入前,有幾種驗證的方法,包括:原生的資料庫約束 (native database constraints) 、客戶端驗證 (client-side validations) 、控制器層級的驗證 (controller-level validations) 、以及 model 層級的驗證 (model-level validations)。

  • 資料庫約束 (database constraints) 也稱作預存程序 (stored procedures) ,這些驗證機制,要依賴特定的資料庫系統,所以測試和維護會比較難。不過,如果資料庫也要讓別的應用程式使用,那在資料庫層做約束可能就是不錯的主意。另外,資料庫層級的驗證可以很安全的處理一些事情,像是在重度使用的資料表中,確保唯一值 (uniqueness) 之類的。這些事情,如果用其他的方法,會比較難實作。
  • 客戶端的驗證 (client-side validations) 雖然可以用,但單單用它的話通常不太可靠。如果用 JavaScript 來實作,要是使用者的瀏覽器把 JavaScript 關掉,那驗證就被跳過了。不過,畢竟客戶端驗證可以給使用者很即時的回饋,所以要是能跟其他驗證手法併用,那倒是挺方便的。
  • 控制器層級的驗證 (controller-level validations) ,也可以用用看,但它常常會變得很笨重,不好測試跟維護。不管怎樣,"keep your controllers skinny":http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model 總是好的,長遠的說,可以讓程式用起來很愉快。
  • model 層級的驗證 (model-level validations) 是最佳的作法。這種驗證方式,不用受制於特定的資料庫系統,也不會被使用者跳過,測試起來很方便,也容易維護。為了方便大家使用 model 層級的驗證,Rails 針對常見的驗證需求,內建了輔助工具,當然,您也可以撰寫自己的驗證方法。

2.2 驗證什麼時候會發生?

Active Record 物件有兩種:對應到資料庫中某筆資料的物件,以及沒有跟資料對應的物件。當您用 new 之類的方法,建立了一個新物件,此時,它還不屬於資料庫。等到您呼叫那個物件的 save 方法,它才會被存入適當的資料表中。至於物件是否已儲存,則是用 Active Record 的實體方法 (instance method) new_record? 來確認。以這個簡單的 Active Record 類別為例:

class Person < ActiveRecord::Base
end

rails console 的輸出結果,可以看出它的運作方式:

>> p = Person.new(:name => "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, :updated_at: nil>
>> p.new_record?
=> true
>> p.save
=> true
>> p.new_record?
=> false

新記錄 (record) 的新增與儲存,會對資料庫送出 SQLINSERT 操作 (operation) 。現有記錄的更新,則會送出 UPDATE 操作。通常,命令送到資料庫之前,會進行驗證,要是任一個驗證失敗,這個物件就標記為無效 (invalid) ,於是 Active Record 就不會執行 INSERTUPDATE ,也就不會讓無效的物件存進資料庫。您也可以針對物件的新增、儲存、更新,指派某幾個特定的驗證。

資料庫裡面,有很多方式,都能改變物件的狀態 (state) 。其中,有些會觸發驗證機制,有些則不會。也就是說,不注意的話,是有可能把無效狀態 (invalid state) 的物件,存到資料庫裡去的。

下面列出的方法,會觸發驗證,驗證合格的物件才會存入資料庫:

  • create
  • create!
  • save
  • save!
  • update
  • update_attributes
  • update_attributes!

其中,方法名稱附有驚嘆號的像是 save! ,會在這筆紀錄 invalid 的時候丟出例外 (raise an exception) 。至於名稱中沒有驚嘆號的,則不會丟出例外,像是 saveupdate_attributes 會傳回 false ,而 createupdate 則單純的傳回物件本身。

2.3 略過驗證

下面列出的方法會略過驗證,不管物件是否有效,都會將它存到資料庫中。所以,要小心使用。

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • update_all
  • update_attribute
  • update_counters

如果,把 :validate => false 當作引數 (argument) 傳入的話, save 方法也是可以略過驗證的。這個技巧也是要小心使用。

  • save(:validate => false)

2.4 valid?invalid? 檢查是否有錯誤訊息

Rails 使用 valid? 方法,來確認物件是否有效,您也可以如法炮製。 valid? 會觸發驗證,沒有錯誤訊息附加到物件上的話,就傳回真值 (true) ,反之,有錯誤訊息的話,就是假值 (false) 。

class Person < ActiveRecord::Base
  validates :name, :presence => true
end

Person.create(:name => "John Doe").valid? # => true
Person.create(:name => nil).valid? # => false

當 Active Record 執行驗證時所找到的錯誤,可以用 errors 實體方法來存取 (accessed) 。驗證結束後,如果錯誤訊息的集合 (collection) 仍是空的,在定義上,這個物件就認定為有效 (valid) 。

要注意的是,用 new 方法實體化 (instantiated) 出來的物件,即使在技術上是無效 (invalid) 的,也不會回報錯誤訊息,因為,執行 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? 的相反。 invalid? 也會觸發驗證,有錯誤訊息加到物件上的話,就傳回真值 (true) , 反之則是假值 (false) 。

2.5 errors[] 屬性內,錯誤訊息的陣列集合

要確認物件的某個屬性 (attribute) 是否有效,可以用 errors[:attribute] 方法,此方法會傳回一個陣列 (array) ,內有該屬性的全部錯誤訊息 (attribute errors)。如果沒有錯誤訊息,則傳回空的陣列。

errors[:attribute] 要在驗證執行過 之後 才能使用,因為,它只檢查錯誤訊息的集合 (errors collection) ,而不會觸發驗證本身。這跟上面提過的 ActiveRecord::Base#invalid? 方法,是不一樣的,因為 errors[:attribute] 不會確認整個物件是否有效,它只是檢查這個物件的某個屬性,有沒有錯誤訊息而已。

class Person < ActiveRecord::Base
  validates :name, :presence => true
end

>> Person.new.errors[:name].any? # => false
>> Person.create.errors[:name].any? # => true

關於驗證所產生的錯誤 (validation errors) ,之後的 Working with Validation Errors 章節會深入解釋。現在,讓我們回頭看看 Rails 內建的驗證輔助工具 (validation helpers) 。

3 驗證輔助工具 (Validation Helpers)

Active Record 預先定義好許多驗證輔助工具,讓我們在類別定義中,直接使用。它們擁有共通的驗證規則,當驗證失敗,就在該物件的 errors 集合中,加入一條錯誤訊息,此訊息跟驗證失敗的欄位 (field) 相關連 (associated) 。

一個輔助工具可以接收 (accepts) 多個屬性名稱 (attribute names) ,數量不拘。所以,只要用一行程式碼,就可以把同一種驗證方式,套用到好幾個不同的屬性上面。

全體輔助工具都會接收 :on:message 選項 (options) ,它們定義了驗證執行的時機,以及驗證失敗時,該加什麼訊息到各自的 errors 集合中。選項 :on 的值,是 :save (預設)、:create:update 的其中之一。而選項 :message 的值,則是自訂的錯誤訊息。每個驗證輔助工具,都擁有預設的錯誤訊息,沒有自訂 :message 選項的話,就會採用預設。接下來,會逐一討論這些輔助工具。

3.1 validates_acceptance_of 確定使用者已經接受

當使用者送出表單時,驗證使用者介面 (user interface) 中的核取方塊 (checkbox) 有被勾選起來。這項驗證,一般用在應用程式的服務條款 (terms of service) ,要使用者確認「我同意」、「我已閱讀」之類的。此驗證是專為網路應用程式 (web applications) 而設計的,這個 ‘acceptance (是否已接受)’ 不須記錄在資料庫內,若資料表中沒有對應的欄位 (field) 也無妨, validates_acceptance_of 輔助工具會建立一個虛擬的屬性 (virtual attribute) 。

class Person < ActiveRecord::Base
  validates_acceptance_of :terms_of_service
end

validates_acceptance_of 預設的錯誤訊息是 “must be accepted” 。

validates_acceptance_of 可以接收 :accept 選項,以決定什麼值代表 acceptance (已接受),預設的值是 “1” ,您也可以改掉它。

class Person < ActiveRecord::Base
  validates_acceptance_of :terms_of_service, :accept => 'yes'
end

3.2 validates_associated 驗證其他有關連的 models

當某個 model 跟其他 models 有關聯 (associations) ,而相關的 models 也需要驗證,那麼,就需要使用這個輔助工具,在物件儲存前,對每個相關聯的物件,呼叫 valid? 方法。

class Library < ActiveRecord::Base
  has_many :books
  validates_associated :books
end

所有的資料庫關聯類型 (association types) ,都可以使用這個驗證。

不要在資料庫關聯的兩端,同時使用 validates_associated ,因為,它們會互相呼叫,最後變成無窮迴圈。

validates_associated 預設的錯誤訊息是 “is invalid” 。每個相關聯的物件 (associated object) 都有自己的 errors 集合,自己的錯誤訊息,只會出現在自己的集合裡面,而不會出現在呼叫方的模型 (calling model) 中。

3.3 validates_confirmation_of 檢查使用者的再次確認

如果有兩個文字欄位必須收到一模一樣的內容,就要用這個驗證。像是要使用者再確認一次 email address 或是 password 的時候。這個驗證會建立一個虛擬屬性 (virtual attribute) ,虛擬屬性的名稱,是需確認欄位的名稱,尾巴加上 “_confirmation” 。

class Person < ActiveRecord::Base
  validates_confirmation_of :email
end

在視圖模板 (view template) 中,可以寫成:

<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>

上述驗證只有在 email_confirmation 不是空值 nil 的時候,才會執行。要確保不是空值,必須為 confirmation 屬性加上存在檢查 (presence check) ,檢查的方式,後面會在 validates_presence_of 講到。

class Person < ActiveRecord::Base
  validates_confirmation_of :email
  validates_presence_of :email_confirmation
end

validates_confirmation_of 預設的錯誤訊息是 “doesn’t match confirmation” 。

3.4 validates_exclusion_of 確定屬性的值不在指定清單內

確認該屬性的值,沒有在指定的集合 (set) 中出現。這個集合可以是任何一種 enumerable 物件。

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

validates_exclusion_of 有個選項 :in ,此選項接收一組值,做為被驗證屬性所不該擁有的值。:in 選項有個別名 (alias) 叫 :within ,功能是一樣的。上面範例的 :message 選項中,示範了如何在錯誤訊息中填入 (include) 屬性的值。

validates_exclusion_of 預設的錯誤訊息是 “is reserved” 。

3.5 validates_format_of 檢查屬性值的格式

測試屬性的值,是否符合 :with 選項所指定的常規表示式 (regular expression) 。

class Product < ActiveRecord::Base
  validates_format_of :legacy_code, :with => /\A[a-zA-Z]+\z/,
    :message => "Only letters allowed"
end

validates_format_of 預設的錯誤訊息是 “is invalid” 。

3.6 validates_inclusion_of 確定屬性的值在指定清單內

確認該屬性的值,有被包括在指定的集合 (set) 裡面,這個集合可以是任何 enumerable 物件。

class Coffee < ActiveRecord::Base
  validates_inclusion_of :size, :in => %w(small medium large),
    :message => "%{value} is not a valid size"
end

validates_inclusion_of 有個選項 :in ,選項的內容,就是該屬性可以擁有的值。 :in 選項有個別名 (alias) 叫做 :within ,功能是相同的。上面範例同樣示範了如何在 :message 錯誤訊息選項中填入 (include) 屬性的值。

validates_inclusion_of 預設的錯誤訊息是 “is not included in the list” 。

3.7 validates_length_of 確定長度在指定範圍內

驗證屬性的值的長度。它提供許多選項,可以指定多種不同的長度限制:

class Person < ActiveRecord::Base
  validates_length_of :name, :minimum => 2
  validates_length_of :bio, :maximum => 500
  validates_length_of :password, :in => 6..20
  validates_length_of :registration_number, :is => 6
end

可用的長度限制選項,包括:

  • :minimum – 屬性的值不能比指定長度短。
  • :maximum – 屬性的值不能比指定長度長。
  • :in (or :within) – 屬性的值的長度,要在某個區間 (interval) 之內。所以,這個選項的值,必須是一個範圍 (range) 。
  • :is – 屬性的值的長度,要跟指定的長度相等。

不同類型的長度驗證,預設的錯誤訊息各自不同。要自訂錯誤訊息,可以用 :wrong_length:too_long:too_short 等選項,搭配 %{count} 做為佔位符 (placeholder) ,帶入關於該長度限制的數字。當然,要繼續用 :message 選項來自訂錯誤訊息,也是可以的。

class Person < ActiveRecord::Base
  validates_length_of :bio, :maximum => 1000,
    :too_long => "%{count} characters is the maximum allowed"
end

預設狀態下,此工具會計算字元 (characters) ,如果要用別的方式切割計算單位,可以利用 :tokenizer 選項:

class Essay < ActiveRecord::Base
  validates_length_of :content,
    :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

Note that the default error messages are plural (e.g., “is too short (minimum is %{count} characters)”). For this reason, when :minimum is 1 you should provide a personalized message or use validates_presence_of instead. When :in or :within have a lower limit of 1, you should either provide a personalized message or call validates_presence_of prior to validates_length_of.

validates_length_of 輔助工具的別名,叫做 validates_size_of

3.8 validates_numericality_of 確保屬性只包括數值

確定某個屬性只包含數值 (numeric values) 。數值的定義,預設為一個可省略的記號 (sign) 後面接一個整數 (integral) 或浮點數 (floating point number) 。如果只想允許整數,把 :only_integer 設定為 true 就可以了。

如果 :only_integer 設為 true ,會用這個常規表示式

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

來驗證屬性的值。反之,則會嘗試用 Float 來把值轉換成數字。

注意上述的常規表示式,是允許換行字元 (trailing newline character) 的。

class Player < ActiveRecord::Base
  validates_numericality_of :points
  validates_numericality_of :games_played, :only_integer => true
end

除了 :only_integer 之外, validates_numericality_of 還接受下列這些選項,為可通過驗證的值追加限制:

  • :greater_than – 必須比這個值還大。預設的錯誤訊息是 “must be greater than %{count}” 。
  • :greater_than_or_equal_to – 必須大於或等於這個值。預設的錯誤訊息是 “must be greater than or equal to %{count}” 。
  • :equal_to – 必須等於這個值。預設的錯誤訊息是 “must be equal to %{count}” 。
  • :less_than – 必須小於這個值。預設的錯誤訊息是 “must be less than %{count}” 。
  • :less_than_or_equal_to – 必須小於或等於這個值。預設的錯誤訊息是 “must be less than or equal to %{count}” 。
  • :odd – 如果設定為 true ,則被驗證的值必須是奇數。預設的錯誤訊息是 “must be odd” 。
  • :even – 如果設定為 true ,則被驗證的值必須是偶數。預設的錯誤訊息是 “must be even” 。

validates_numericality_of 預設的錯誤訊息是 “is not a number” 。

3.9 validates_presence_of 確定屬性是存在的

驗證指定的屬性不是空的。此工具使用 blank? 方法,去檢查屬性值是否為 nil 或者空白字串 (blank string) , nil 指的是沒有內容 (empty) ,空白字串指的是由空白字元 (whitespace) 組成的字串。

class Person < ActiveRecord::Base
  validates :name, :login, :email, :presence => true
end

如果要確認某個資料庫關聯 (association) 存在,那麼該驗證的對象,是和此關聯相對應 (map) 的外部鍵 (foreign key) ,而不是相關聯的物件 (associated object) 本身。

class LineItem < ActiveRecord::Base
  belongs_to :order
  validates_presence_of :order_id
end

由於 false.blank? 的結果是 true ,也就是說,如果屬性的值剛好是 false ,則會被 blank? 方法視為是空的值。所以,若要驗證一個布林欄位 (boolean field) 是否存在的話,必須寫成 validates_inclusion_of :field_name, :in => [true, false]

validates_presence_of 預設的錯誤訊息是 “can’t be empty” 。

3.10 validates_uniqueness_of 確保屬性的值是唯一的

在物件將要儲存時,驗證屬性的值,以確定它是唯一的。這個工具,並不會在資料庫中建立唯一值約束 (uniqueness constraint) ,所以,可能會發生兩個不同資料庫連結 (database connections) 在同一欄存入了兩筆相同記錄 (records) 的狀況。要避免這個狀況,必須在資料庫內,製作唯一索引 (unique index) 。

class Account < ActiveRecord::Base
  validates_uniqueness_of :email
end

此驗證會對 model 的資料表執行 SQL 查詢 (query) ,在被驗證屬性的現存記錄中,搜尋相同的值。

此驗證有個 :scope 選項,可以指定其他的屬性,以便為唯一值檢查加上額外的限制:

class Holiday < ActiveRecord::Base
  validates_uniqueness_of :name, :scope => :year,
    :message => "should happen once per year"
end

此外還有 :case_sensitive 選項,可以設定唯一值的檢查是否要區分大小寫 (case sensitive) 。預設是 true 。

class Person < ActiveRecord::Base
  validates_uniqueness_of :name, :case_sensitive => false
end

要注意的是,有的資料庫系統,不論如何,都會執行不分大小寫 (case-insensitive) 的搜尋。

validates_uniqueness_of 預設的錯誤訊息是 “has already been taken” 。

3.11 validates_with 用另外的類別來做驗證

把記錄 (record) 傳遞給另一個獨立的類別 (separate class) 來進行驗證。

class Person < ActiveRecord::Base
  validates_with GoodnessValidator
end

class GoodnessValidator < ActiveModel::Validator
  def validate
    if record.first_name == "Evil"
      record.errors[:base] << "This person is evil"
    end
  end
end

validates_with 可以用單一類別、或一組類別清單來進行驗證。它沒有預設錯誤訊息,所以,必須手動在 validator 類別裡,把錯誤訊息加入該筆記錄的錯誤訊息集合 (errors collection) 中。

validator 類別預設有兩個屬性:

  • record – 要驗證的那筆記錄 (record)
  • options – 傳遞給 validates_with 的額外選項 (extra options)

validates_with 跟其他驗證方式一樣,有 :if:unless:on 這三個選項。如果傳入其他選項,則會做為 options 傳送到 validator 類別去:

class Person < ActiveRecord::Base
  validates_with GoodnessValidator, :fields => [:first_name, :last_name]
end

class GoodnessValidator < ActiveRecord::Validator
  def validate
    if options[:fields].any?{|field| record.send(field) == "Evil" }
      record.errors[:base] << "This person is evil"
    end
  end
end

3.12 validates_each 用區塊進行自訂的驗證

利用程式碼區塊 (block) 驗證屬性 (attributes) 。這個工具,沒有預先定義驗證功能,要自己用區塊撰寫,之後,傳遞給 validates_each 的屬性會用這個區塊來測試。下面的範例,希望 names 跟 surnames 不要有小寫開頭:

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

區塊會接收三樣東西:model 、屬性的名稱、屬性的值。區塊內可以做任何驗證,驗證失敗的話,可以在 model 裡加入錯誤訊息,將它標記為無效 (invalid) 。

4 共通的驗證選項 (Common Validation Options)

有些共通的選項 (options) ,所有驗證輔助工具都能使用。下面介紹除了 :if:unless 之外的共通選項,至於 :if:unless 會另外在 條件式驗證 的部份討論。

4.1 :allow_nil 允許 nil 值

當被驗證的值是 nil 時,略過驗證。在 validates_presence_of 工具裡使用 :allow_nil 選項的話,就可以允許 nil 值,不過,其他的 blank? 值仍然會被駁回 (rejected) 。

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

4.2 :allow_blank 允許 blank 值

:allow_blank 有點類似 :allow_nil ,但允許的範圍更廣。 :allow_blank 會在屬性值是 blank? 時,讓驗證通過,像是 nil 或空字串 (empty string) 都會允許。

class Topic < ActiveRecord::Base
  validates_length_of :title, :is => 5, :allow_blank => true
end

Topic.create("title" => "").valid? # => true
Topic.create("title" => nil).valid? # => true

4.3 :message 自訂錯誤訊息

:message 選項用來自訂錯誤訊息,以便在驗證失敗時,將自訂的訊息加入 errors 集合 (collection) 裡面。沒有設定此選項的話, Active Record 會採用該驗證輔助工具 (validation helpers) 原本內建的錯誤訊息。

4.4 :on 在特定的時候進行驗證

:on 選項指定驗證發生的時機。內建的驗證輔助工具,預設的行為是在儲存的時候執行驗證,包括新增記錄、以及更新記錄。您可以用 :on => :create 指定只要在新增的時候驗證,或者用 :on => :update 設定只要在更新的時候驗證。

class Person < ActiveRecord::Base
  # 允許在更新 email 資料的時候,輸入跟先前一樣的 email 值
  validates_uniqueness_of :email, :on => :create

  # 允許在新增年齡資料的時候,輸入不是數字 (non-numerical) 的值
  validates_numericality_of :age, :on => :update

  # 預設的狀況(新增、更新,兩種狀況都驗證)
  validates :name, :presence => true, :on => :save
end

5 條件式驗證 (Conditional Validation)

有時候,在某些先決條件 (predicate) 之下,才需要驗證物件,此時就可以用 :if:unless 選項,選項內容可以是符號 (symbol) 、字串 (string) 、或 Proc 程式物件。 :if 選項用來指定驗證何時 應該 發生,而 :unless 選項則指定驗證何時 不應該 發生。

5.1 在 :if:unless 中使用符號 (Symbol)

把某個方法的名稱,以符號的形式,放入 (associate) :if:unless 選項內,就可以在驗證前呼叫這個方法,來判斷是否要執行驗證。這是最常見的一種條件式寫法。

class Order < ActiveRecord::Base
  validates_presence_of :card_number, :if => :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

5.2 在 :if:unless 中使用字串 (String)

把一段有效的 Ruby 程式碼,以字串的形式,放入條件式選項內,就可以將它丟去給 eval 求值,來判斷是否要執行驗證。這種寫法,只適合在字串條件式很短的時候使用。

class Person < ActiveRecord::Base
  validates_presence_of :surname, :if => "name.nil?"
end

5.3 在 :if:unless 中使用 Proc 物件

最後,把 Proc 物件放入 :if:unless 選項內,就可以呼叫這個物件,來判斷是否要執行驗證。使用 Proc 物件,就可以直接撰寫判斷式 (inline condition) ,而不需要另闢一個獨立的方法 (separate method) 。這種寫法,非常適合僅有一行的程式 (one-liners) 。

class Account < ActiveRecord::Base
  validates_confirmation_of :password,
    :unless => Proc.new { |a| a.password.blank? }
end

6 打造自訂的驗證方法

內建的工具不夠用的話,您可以自己撰寫驗證方法。

步驟很簡單,先建立方法 (methods) ,來確認 models 的狀態 (state) ,並在 model 無效 (invalid) 時,把錯誤訊息加入 errors 集合。然後,將自訂方法的名稱,以符號 (symbols) 的形式,傳入 validatevalidate_on_createvalidate_on_update 等類別方法 (class methods) 中,以便註冊 (register) 自訂方法。

在類別方法 (class method) 中,可以傳入多個符號 (symbol) ,跟符號對應的驗證方法,會依照它們註冊的順序來執行。

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
    errors.add(:expiration_date, "can't be in the past") if
      !expiration_date.blank? and expiration_date < Date.today
  end

  def discount_cannot_be_greater_than_total_value
    errors.add(:discount, "can't be greater than total value") if
      discount > total_value
  end
end

除了撰寫自訂方法,還可以更進一步,撰寫自訂的驗證輔助工具,然後在幾個不同的 model 之間重複使用。例如,管理問卷調查的應用程式,需要讓一個欄位 (field) 對應到一整組選項:

ActiveRecord::Base.class_eval do
  def self.validates_as_choice(attr_name, n, options={})
    validates_inclusion_of attr_name, {:in => 1..n}.merge(options)
  end
end

只要打開 ActiveRecord::Base ,定義如上述的類別方法,自訂的輔助工具就完成了。通常,這段程式碼會放在 config/initializers 裡面。而它的用法,就像這樣:

class Movie < ActiveRecord::Base
  validates_as_choice :rating, 5
end

7 處理驗證錯誤

除了前面提過的 valid?invalid? 方法之外, Rails 還提供許多其他方法,來處理 errors 集合,或者查詢物件的有效性 (validity) 。

下面只列出最常用的方法,至於完整的方法列表,請參考 ActiveRecord::Errors 的文件。

7.1 errors 列出物件的錯誤訊息

把某個物件的所有錯誤訊息,回傳為一個有序雜湊 (OrderedHash) 。雜湊的鍵 (key) 是屬性的名稱 (attribute name) ,值 (value) 則是一個陣列,陣列由字串組成,內有該屬性的所有錯誤訊息。

class Person < ActiveRecord::Base
  validates :name, :presence => true
  validates_length_of :name, :minimum => 3
end

person = Person.new
person.valid? # => false
person.errors
 # => {:name => ["can't be blank", "is too short (minimum is 3 characters)"]}

person = Person.new(:name => "John Doe")
person.valid? # => true
person.errors # => []

7.2 errors[] 列出屬性的錯誤訊息

把某個特定屬性的錯誤訊息,回傳為一個陣列 (array) ,陣列由字串組成,每個字串是一筆錯誤訊息。如果這個屬性沒有錯誤訊息,則會傳回一個空的陣列。

class Person < ActiveRecord::Base
  validates :name, :presence => true
  validates_length_of :name, :minimum => 3
end

person = Person.new(:name => "John Doe")
person.valid? # => true
person.errors[:name] # => []

person = Person.new(:name => "JD")
person.valid? # => false
person.errors[:name] # => ["is too short (minimum is 3 characters)"]

person = Person.new
person.valid? # => false
person.errors[:name]
 # => ["can't be blank", "is too short (minimum is 3 characters)"]

7.3 errors.add 手動加入錯誤訊息

add 方法可以針對某個特定屬性,用手動的方式,加入錯誤訊息。這些訊息在使用者眼中的樣子,可以用 errors.full_messageserrors.to_a 這兩種方法來預覽,預覽時可以看到屬性名稱的開頭變成了大寫,並附加在錯誤訊息的前面。 add 方法接收兩樣東西:一是屬性名稱,二是您想手動附上的錯誤訊息。

class Person < ActiveRecord::Base
  def a_method_used_for_validation_purposes
    errors.add(:name, "cannot contain the characters [email protected]#%*()_-+=")
  end
end

person = Person.create(:name => "[email protected]#")

person.errors[:name]
 # => ["cannot contain the characters [email protected]#%*()_-+="]

person.errors.full_messages
 # => ["Name cannot contain the characters [email protected]#%*()_-+="]

手動加入訊息的方式,除了 add 之外,還可以用 []= 設定式 (setter) :

class Person < ActiveRecord::Base
    def a_method_used_for_validation_purposes
      errors[:name] = "cannot contain the characters [email protected]#%*()_-+="
    end
  end

  person = Person.create(:name => "[email protected]#")

  person.errors[:name]
   # => ["cannot contain the characters [email protected]#%*()_-+="]

  person.errors.to_a
   # => ["Name cannot contain the characters [email protected]#%*()_-+="]

7.4 errors[:base] 針對整個物件的錯誤訊息

一般的錯誤訊息是關連 (related) 到某一屬性,不過,也可以加入關連到整個物件狀態 (object’s state) 的錯誤訊息。有時,不需知道各屬性的細節,只需表示出這個物件是無效的,這時候就可以用 errors[:base] ,它是一個陣列,只要在陣列中加入字串,就可以將字串拿來當做物件的錯誤訊息。

class Person < ActiveRecord::Base
  def a_method_used_for_validation_purposes
    errors[:base] << "This person is invalid because ..."
  end
end

7.5 errors.clear 清空錯誤訊息

clear 方法可以刻意地清空 errors 集合裡的所有錯誤訊息。清空後,原本無效的物件並不會變成有效,因為,即使錯誤訊息清空了,下次呼叫 valid? 方法時,或者想把物件寫入資料庫時,都會再次的觸發驗證,一旦驗證失敗, errors 集合就會再次的被填滿。

class Person < ActiveRecord::Base
  validates :name, :presence => true
  validates_length_of :name, :minimum => 3
end

person = Person.new
person.valid? # => false
person.errors[:name]
 # => ["can't be blank", "is too short (minimum is 3 characters)"]

person.errors.clear
person.errors.empty? # => true

p.save # => false

p.errors[:name]
 # => ["can't be blank", "is too short (minimum is 3 characters)"]

7.6 errors.size 物件的錯誤訊息總數

size 方法會傳回該物件的錯誤訊息總數。

class Person < ActiveRecord::Base
  validates :name, :presence => true
  validates_length_of   :name, :minimum => 3
  validates_presence_of :email
end

person = Person.new
person.valid? # => false
person.errors.size # => 3

person = Person.new(:name => "Andrea", :email => "[email protected]")
person.valid? # => true
person.errors.size # => 0

8 在視圖中秀出驗證錯誤 (Displaying Validation Errors in the View)

Rails 內建一些輔助工具,可以在視圖模板 (view templates) 中,顯示 models 的錯誤訊息 (error messages) 。

8.1 error_messageserror_messages_for 在視圖中呈現錯誤訊息

form_for 輔助工具新增表單的時候,可以在表單產生器 (form builder) 裡,用 error_messages 方法,把目前這個 model 實體 (instance) 全部的驗證失敗訊息,演算呈現 (render) 出來。

class Product < ActiveRecord::Base
  validates_presence_of :description, :value
  validates_numericality_of :value, :allow_nil => true
end
<%= form_for(@product) do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :description %><br />
    <%= f.text_field :description %>
  </p>
  <p>
    <%= f.label :value %><br />
    <%= f.text_field :value %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

送出上述表單時,如果表單內有空白欄位 (empty fields) ,會得到如下圖般的回應。不過,預設狀況下,這個頁面的樣式 (styles) 其實應該是從缺的:

Error messages

另外,也可以用 error_messages_for 輔助工具,針對指派給視圖模板 (view template) 的 model ,顯示它的錯誤訊息。功能跟上面的範例很像,最後結果也完全相同:

<%= error_messages_for :product %>

呈現錯誤訊息的文字,格式是「大寫開頭的屬性名稱 + 錯誤訊息」。

form.error_messageserror_messages_for 這兩個輔助工具,都可以接收一些自訂的選項,以便針對包裹著訊息文字的 div 元素 (element) ,自訂標題文字 (header text) 、標題文字下方的訊息、以及用來定義標題元素的標籤 (tag) 。

<%= f.error_messages :header_message => "Invalid product!",
  :message => "You'll need to fix the following fields:",
  :header_tag => :h3 %>

會產生以下內容:

Customized error messages

如果對任一個選項傳入 nil 值,則會拋棄 div 中與該選項相對應的區段 (section) 。

8.2 自訂錯誤訊息的串接樣式表 (CSS)

用來自訂錯誤訊息樣式 (styles) 的選擇器 (selectors) 包括:

  • .field_with_errors – 產生錯誤的表格欄位 (form fields) 與欄位標籤 (labels) 的樣式 (style) 。
  • #errorExplanation – 包覆著錯誤訊息的 div 元素的樣式。
  • #errorExplanation h2div 元素之內的標題 (header) 的樣式。
  • #errorExplanation pdiv 元素之內,緊接著出現在標題 (header) 下方的段落文字 (paragraph) 的樣式。
  • #errorExplanation ul li – 列出各個錯誤訊息的項目清單 (list items) 的樣式。

以 Rails 內建的鷹架 (scaffolding) 為例,會產生 public/stylesheets/scaffold.css 這份樣式表 (CSS) ,定義了上圖的紅色系樣式。

樣式表 (CSS) 裡面的 class 名稱跟 id 名稱,可以用 :class:id 選項來修改,這二個選項在 form_forerror_messages_for 兩個輔助工具中,都可以使用。

8.3 自訂錯誤訊息的 HTML

按照預設,產生錯誤的表單欄位 (form fields) ,會被包在一個 div 元素裡面,此 divCSS class 是 field_with_errors 。這個樣式設定,是可以覆寫 (override) 過去的。

產生錯誤的表單欄位,應該如何處理,是定義在 ActionView::Base.field_error_proc 裡面的,這是一個 Proc 物件,可以接收兩個參數 (parameters) :

  • 帶有 HTML 標籤 (tag) 的字串 (string) 。
  • ActionView::Helpers::InstanceTag 類別的實體 (instance) 。

下面的範例,把 Rails 的行為改成「總是把錯誤訊息,顯示在產生錯誤的表單欄位之前」,錯誤訊息用 span 元素包起來, span 元素的 CSS class 是 validation-errorinput 元素沒有用 div 元素包裹,所以文字欄位的周圍不會有紅色邊框。總之,想要什麼樣式,都可以用 validation-error 這個 CSS class 來做設定。

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  if instance.error_message.kind_of?(Array)
    %(#{html_tag}<span class="validation-error">&nbsp;
      #{instance.error_message.join(',')}</span>).html_safe
  else
    %(#{html_tag}<span class="validation-error">&nbsp;
      #{instance.error_message}</span>).html_safe
  end
end

這段程式碼會產生如下圖的結果:

Validation error messages

9 回呼概觀 (Callbacks Overview)

回呼 (callbacks) 是在物件生命週期裡的特定時刻,所被呼叫的方法。有了它,就可以在 Active Record 物件新增、儲存、更新、刪除、驗證、或者從資料庫載入時,執行我們自己撰寫的程式碼。

9.1 註冊回呼 (Callback Registration)

使用回呼 (callback) 之前,必須先註冊 (register) 它。註冊的方式,可以先將回呼寫成一般正常的方法 (methods) ,然後用巨集風格的類別方法 (macro-style class method) ,將它們註冊成回呼。

class User < ActiveRecord::Base
  validates_presence_of :login, :email

  before_validation :ensure_login_has_a_value

  protected
  def ensure_login_has_a_value
    if login.nil?
      self.login = email unless email.blank?
    end
  end
end

巨集風格的類別方法 (macro-style class methods) 也可以接收程式碼區塊 (block) ,如果區塊內的程式碼很短,短到可以一行之內寫完,那麼就可以考慮用區塊的方式來註冊回呼。

class User < ActiveRecord::Base
  validates_presence_of :login, :email

  before_create {|user| user.name = user.login.capitalize
	if user.name.blank?}
end

把回呼方法 (callback methods) 宣告為 protected 或 private ,是比較好的作法。若是預設的 public ,那麼在 model 之外的地方,也可以呼叫這些方法,這樣一來,會侵犯到物件封裝 (object encapsulation) 的原則。

10 可供使用的回呼 (Available Callbacks)

這裡列出 Active Record 的回呼 (callbacks) ,列出的順序,即是這些方法在建立、更新、銷毀等操作 (operations) 中,所被呼叫的順序。

10.1 建立物件時 (Creating an Object)

  • before_validation
  • after_validation
  • before_save
  • after_save
  • before_create
  • around_create
  • after_create

10.2 更新物件時 (Updating an Object)

  • before_validation
  • after_validation
  • before_save
  • after_save
  • before_update
  • around_update
  • after_update

10.3 銷毀物件時 (Destroying an Object)

  • before_destroy
  • after_destroy
  • around_destroy

after_save 在建立、更新時,都會運作,但是,不論宣告的順序為何,after_save 永遠在更特定的 after_createafter_update之後 才會執行。

10.4 after_initializeafter_find

Active Record 物件初始化的時候,不管是直接用 new 初始化,或是從資料庫中載入一筆記錄 (record) 來初始化,都會呼叫 after_initialize 這個回呼 (callback) 。它的好處是,讓我們不需要直接覆蓋 (override) 掉 Active Record 原本的 initialize 方法。

Active Record 從資料庫中載入記錄時,一定會呼叫 after_find 回呼。如果, after_findafter_initialize 都有定義,則會先呼叫 after_find

after_initializeafter_find 跟其他的回呼有些不同。它們沒有相對的 before_* 回呼,而它們註冊的方式,只有一種,就是將它們定義為一般的方法 (regular methods) 。如果,用巨集風格的類別方法 (macro-style class methods) 來註冊 after_initializeafter_find ,那麼它們會直接被忽略。設計這樣的行為,是為了效能上的考量,因為,在資料庫中每找到一筆記錄,都會呼叫 after_initializeafter_find ,而明顯拖慢查詢的速度。

class User < ActiveRecord::Base
  def after_initialize
    puts "You have initialized an object!"
  end

  def after_find
    puts "You have found an object!"
  end
end

>> User.new
You have initialized an object!
=> #<User id: nil>

>> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>

11 執行回呼 (Running Callbacks)

以下這些方法,會觸發 (trigger) 回呼:

  • create
  • create!
  • decrement!
  • destroy
  • destroy_all
  • increment!
  • save
  • save!
  • save(false)
  • toggle!
  • update
  • update_attribute
  • update_attributes
  • update_attributes!
  • valid?

另外,以下這些查找方法 (finder methods),會觸發 after_find 這個回呼:

  • all
  • first
  • find
  • find_all_by_attribute
  • find_by_attribute
  • find_by_attribute!
  • last

至於 after_initialize 回呼,被觸發的時機,則是在該類別每一次初始化 (initialized) 新物件的時候。

12 略過回呼 (Skipping Callbacks)

回呼 (callbacks) 是可以略過的,就跟驗證一樣。不過,用這些略過回呼的方法時,必須很小心,因為有些重要的商業規則 (business rules) 跟應用程式邏輯 (application logic) ,可能會放在回呼之中,如果不清楚會造成什麼影響,就略過回呼的話,可能會導致無效資料 (invalid data) 的產生。

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • find_by_sql
  • increment
  • increment_counter
  • toggle
  • update_all
  • update_counters

13 中止執行鏈 (Halting Execution)

為 models 註冊了新的回呼後,它們會被加入佇列 (queued) 以等候執行。佇列 (queue) 裡面包含:該 model 全部的驗證、已註冊的回呼、以及即將執行的資料庫操作 (database operation) 。

整個回呼鏈 (callback chain) 會被打包在一個交易功能 (transaction) 中,如果任一個 before 回呼方法傳回了 false ,或者丟出了例外 (raises an exception) ,整個執行鏈 (execution chain) 就會中途停止 (halted) ,且會發送出一個資料庫 ROLLBACK。至於 after 回呼方法,則是只有在丟出例外的時候,才會導致執行鏈的中止。

丟出任何例外 (exception) 都會破壞 save 和回呼鏈的執行。除了 ActiveRecord::Rollback 這個例外之外,它會精確地時告訴 Active Record 只進行滾回(rollback)動作。這個例外(exception) 會被 ActiveRecord 內部所捕捉而不會繼續往外丟出。

14 關連性回呼 (Relational Callbacks)

回呼 (callbacks) 能夠透過 model 間的關連 (relationships) 運作,甚至可以利用它們來定義回呼。例如,有個使用者,發表了許多篇文章,我們希望使用者帳號銷毀時,文章也跟著銷毀。於是,在 User model 中,經由它跟 Post model 的關連,加入 after_destroy 回呼。範例如下:

class User < ActiveRecord::Base
  has_many :posts, :dependent => :destroy
end

class Post < ActiveRecord::Base
  after_destroy :log_destroy_action

  def log_destroy_action
    puts 'Post destroyed'
  end
end

>> user = User.first
=> #<User id: 1>
>> user.posts.create!
=> #<Post id: 1, user_id: 1>
>> user.destroy
Post destroyed
=> #<User id: 1>

15 條件式回呼 (Conditional Callbacks)

和驗證一樣,回呼也可以是條件式的,在某個先決條件成立時,才呼叫它。條件式可以用 :if:unless 選項來做,選項可以接收符號 (symbol) 、字串 (string) 、或 Proc 物件。在某狀況下 必須 呼叫回呼,可以用 :if 選項;在某狀況下 必須不 呼叫回呼,則是用 :unless 選項。

15.1 在 :if:unless 中使用符號 (Symbol)

:if:unless 可以放入 (associate) 符號 (symbol) ,符號對應到回呼執行前所呼叫的方法 (method) 的名稱。使用 :if 時,若此方法傳回假值 (false) ,回呼就不會執行;相反地,使用 :unless 的話,若此方法傳回真值 (true) ,回呼就不會執行。以上是最常用到的作法。用這種格式的話,可以同時註冊好幾個不同的方法,來判斷是否要執行回呼。

class Order < ActiveRecord::Base
  before_save :normalize_card_number, :if => :paid_with_card?
end

15.2 在 :if:unless 中使用字串 (String)

條件式裡面也可以放字串 (string) ,字串會用 eval 來求值 (evaluated),字串內容必須是一段有效的 Ruby 程式碼。只有真的很簡短的條件式,才適合用字串的形式,放在選項裡面。

class Order < ActiveRecord::Base
  before_save :normalize_card_number, :if => "paid_with_card?"
end

15.3 在 :if:unless 中使用 Proc 物件

最後, :if:unless 還可以放入 Proc 物件。這種作法,特別適合簡短的驗證方法,通常是一行以內的 (one-liners) 。

class Order < ActiveRecord::Base
  before_save :normalize_card_number,
    :if => Proc.new { |order| order.paid_with_card? }
end

15.4 回呼的多重條件 (Multiple Conditions for Callbacks)

撰寫條件式回呼時,可以在同一個回呼宣告 (callback declaration) 裡面,混合使用 :if:unless

class Comment < ActiveRecord::Base
  after_create :send_email_to_author, :if => :author_wants_emails?,
    :unless => Proc.new { |comment| comment.post.ignore_comments? }
end

16 回呼類別 (Callback Classes)

有些 model 的回呼方法,很值得在其他 model 中重複使用。 Active Record 讓我們建立回呼類別 (callback classes) ,封裝這些回呼方法,讓它們易於重複使用。

底下範例建立一個類別,內含 PictureFile model 的 after_destroy 回呼。

class PictureFileCallbacks
  def after_destroy(picture_file)
    File.delete(picture_file.filepath)
      if File.exists?(picture_file.filepath)
  end
end

在類別內部宣告回呼方法時,回呼方法會接收 model 物件做為參數 (parameter) ,在這裡可以這樣用:

class PictureFile < ActiveRecord::Base
  after_destroy PictureFileCallbacks.new
end

由於回呼是宣告為實體方法 (instance method) ,所以使用時,必須先實體化 (instantiate) 一個新的 PictureFileCallbacks 物件。有些時候,把回呼宣告為類別方法 (class method) 會比較合理些。

class PictureFileCallbacks
  def self.after_destroy(picture_file)
    File.delete(picture_file.filepath)
      if File.exists?(picture_file.filepath)
  end
end

用上述方式宣告回呼,就不需要把 PictureFileCallbacks 物件實體化 (instantiate) 了。

class PictureFile < ActiveRecord::Base
  after_destroy PictureFileCallbacks
end

回呼類別 (callback classes) 裡,可宣告的回呼數量沒有限制,要幾個都可以。

17 觀察者 (Observers)

觀察者 (observers) 跟回呼很像,但有個重大的差別在於,回呼的程式碼會污染 (pollute) model ,而這些程式碼,往往與 model 本身的用途,並沒有直接的關係;但是,觀察者 (observers) 卻可以在 model 的外部,以不會污染到 model 的方式,做到跟回呼一樣的功能。像是 User model 裡面,其實不該有送出註冊確認信 (registration confirmation emails) 的程式碼。當回呼內的程式碼跟 model 本身沒有直接關係,就可以考慮用觀察者 (observer) 來代替回呼。

17.1 建立觀察者 (Creating Observers)

假設有個 User model ,希望每次它新增使用者的時候,系統都送出一封確認 email 。由於寄信並不是 User model 存在的目的,所以,我們可以用觀察者 (observer) 來加入這個功能。

rails generate observer User
class UserObserver < ActiveRecord::Observer
  def after_create(model)
    # code to send confirmation email...
  end
end

觀察者的方法,將所觀測的 model 接收為一個參數,這一點,跟回呼類別 (callback classes) 相同。

17.2 註冊觀察者 (Registering Observers)

依照慣例,觀察者會放在 app/models 目錄下,並且在應用程式的 config/environment.rb 檔案中註冊。例如,上面的 UserObserver ,會儲存成 app/models/user_observer.rb ,然後在 config/environment.rb 中註冊,像這樣:

# Activate observers that should always be running
config.active_record.observers = :user_observer

config/environments 內的設定,優先權 (precedence) 會高於 config/environment.rb 。因此,如果某個觀察者 (observer) 並不需要在所有的環境 (environments) 內執行,那麼可以只在某個特定的環境內註冊就好。

17.3 共用觀察者 (Sharing Observers)

按照預設,Rails 會把觀察者的名稱,直接除掉 “Observer” 後,做為所要觀測的 model 名稱。不過,觀察者其實可以對上多個 models ,而我們可以手動的指定這些 models 。

class MailerObserver < ActiveRecord::Observer
  observe :registration, :user

  def after_create(model)
    # code to send confirmation email...
  end
end

這個範例中,不管是新建立 RegistrationUser ,都會呼叫 after_create 方法。要注意的是,新出現的 MailerObserver 也要在 config/environment.rb 中註冊,才會產生作用。

# Activate observers that should always be running
config.active_record.observers = :mailer_observer

18 Transaction Callbacks

There are two additional callbacks that are triggered by the completion of a database transaction: after_commit and after_rollback. These callbacks are very similar to the after_save callback except that they don’t execute until after database changes have either been committed or rolled back. They are most useful when your active record models need to interact with external systems which are not part of the database transaction.

Consider, for example, the previous example where the PictureFile model needs to delete a file after a record is destroyed. If anything raises an exception after the after_destroy callback is called and the transaction rolls back, the file will have been deleted and the model will be left in an inconsistent state. For example, suppose that picture_file_2 in the code below is not valid and the save! method raises an error.

PictureFile.transaction do
  picture_file_1.destroy
  picture_file_2.save!
end

By using the after_commit callback we can account for this case.

class PictureFile < ActiveRecord::Base
  attr_accessor :delete_file

  after_destroy do |picture_file|
    picture_file.delete_file = picture_file.filepath
  end

  after_commit do |picture_file|
    if picture_file.delete_file && File.exist?(picture_file.delete_file)
      File.delete(picture_file.delete_file)
      picture_file.delete_file = nil
    end
  end
end

The after_commit and after_rollback callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don’t interfere with the other callbacks. As such, if your callback code could raise an exception, you’ll need to rescue it and handle it appropriately within the callback.

19 文件修改記錄 (Changelog)

Lighthouse ticket

  • February 17, 2011: Add description of transaction callbacks.
  • July 20, 2010: Fixed typos and rephrased some paragraphs for clarity. Jaime Iniesta
  • May 24, 2010: Fixed document to validate XHTML 1.0 Strict. Jaime Iniesta
  • May 15, 2010: Validation Errors section updated by Emili Parreño
  • March 7, 2009: Callbacks revision by Trevor Turk
  • February 10, 2009: Observers revision by Trevor Turk
  • February 5, 2009: Initial revision by Trevor Turk
  • January 9, 2009: Initial version by Cássio Marques

20 關於譯者

21 翻譯詞彙

本文翻譯自 http://edgeguides.rubyonrails.org/active_record_validations_callbacks.html

英文與繁體中文的對照詞彙,請見 rails-guide-glossary-list-zh_tw

部份翻譯詞彙參考自 Ruby Programming-向Ruby之父學程式設計 以及 松本行弘的程式世界:成為一流程式設計師的14種思考術 二書,皆為博碩出版。