v7.0.0
更多資訊請前往 rubyonrails.org: 更多在 Ruby on Rails

Active Record 回呼(Callbacks)

本指南教你如何掛載你的Active Record的生命週期 物件。

閱讀本指南後,您將瞭解:

1 物件生命週期

在 Rails 應用程式的正常執行期間,可能會建立、更新和銷燬物件。 Active Record 為這個物件生命週期提供了掛載機制,以便您可以控制您的應用程式及其資料。

Callbacks 允許您在更改物件狀態之前或之後觸發邏輯。

2 Callback 概述

Callback 是在物件生命週期的特定時刻被呼叫的方法。使用回呼,可以編寫在建立、儲存、更新、刪除、驗證或從資料庫載入 Active Record 物件時執行的程式碼。

2.1 Callback 註冊

為了使用可用的callbacks,您需要註冊它們。您可以將 callbacks 實現為普通方法,並使用宏樣式的類方法將它們註冊為 callbacks:

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_validation :ensure_login_has_a_value

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

宏風格的類方法也可以接收一個塊。如果程式碼塊內的程式碼太短以至於可以放在一行中,請考慮使用這種樣式:

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_create do
    self.name = login.capitalize if name.blank?
  end
end

Callbacks 也可以註冊為僅在某些生命週期事件上觸發:

class User < ApplicationRecord
  before_validation :normalize_name, on: :create

  # :on takes an array as well
  after_validation :set_location, on: [ :create, :update ]

  private
    def normalize_name
      self.name = name.downcase.titleize
    end

    def set_location
      self.location = LocationService.query(self)
    end
end

將 callback 方法宣告為私有方法被認為是一種很好的做法。如果將它們設為 public,則可以從 model 外部呼叫它們,並且違反了物件封裝原則。

3 可用 Callbacks

這是一個包含所有可用 Active Record callbacks 的列表,按照在相應操作期間呼叫它們的相同順序列出:

3.1 建立物件

3.2 更新物件

3.3 銷燬物件

after_save 在建立和更新時執行,但總是after 更具體的 callbacks after_createafter_update,無論宏呼叫的執行順序如何。

避免在 callbacks 中更新或儲存屬性。例如,不要在回呼中呼叫 update(attribute: "value")。這可能會改變 model 的狀態,並可能在提交期間導致意外的副作用。相反,您可以在 before_create / before_update 或更早的 callbacks 中直接安全地分配 values(例如,self.attribute = "value")。

before_destroy callbacks 應該放在 dependent: :destroy 之前 associations(或使用 prepend: true 選項),以確保它們在執行之前 記錄被 dependent: :destroy 刪除。

3.4 after_initializeafter_find

after_initialize callback 將在實例化 Active Record 物件時呼叫,直接使用 new 或從資料庫載入記錄時。避免直接覆蓋 Active Record initialize 方法的需要會很有用。

每當 Active Record 從資料庫載入記錄時,都會呼叫 after_find callback。如果兩者都定義了,則 after_findafter_initialize 之前呼叫。

after_initializeafter_find callbacks 沒有對應的 before_*,但它們可以像其他 Active Record callbacks 一樣註冊。

class User < ApplicationRecord
  after_initialize do |user|
    puts "You have initialized an object!"
  end

  after_find do |user|
    puts "You have found an object!"
  end
end
irb> User.new
You have initialized an object!
=> #<User id: nil>

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

3.5 after_touch

每當觸控 Active Record 物件時,都會呼叫 after_touch callback。

class User < ApplicationRecord
  after_touch do |user|
    puts "You have touched an object"
  end
end
irb> u = User.create(name: 'Kuldeep')
=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">

irb> u.touch
You have touched an object
=> true

它可以與 belongs_to 一起使用:

class Employee < ApplicationRecord
  belongs_to :company, touch: true
  after_touch do
    puts 'An Employee was touched'
  end
end

class Company < ApplicationRecord
  has_many :employees
  after_touch :log_when_employees_or_company_touched

  private
    def log_when_employees_or_company_touched
      puts 'Employee/Company was touched'
    end
end
irb> @employee = Employee.last
=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">

irb> @employee.touch # triggers @employee.company.touch
An Employee was touched
Employee/Company was touched
=> true

4 執行 Callbacks

以下方法會觸發 callbacks:

  • create
  • create!
  • destroy
  • destroy!
  • destroy_all
  • destroy_by
  • save
  • save!
  • save(validate: false)
  • toggle!
  • touch
  • update_attribute
  • update
  • update!
  • valid?

此外,after_find callback 由以下查詢器方法觸發:

  • all
  • first
  • find
  • find_by
  • find_by_*
  • find_by_*!
  • find_by_sql
  • last

每次初始化類的新物件時都會觸發 after_initialize callback。

find_by_*find_by_*! 方法是為每個屬性自動生成的動態查詢器。在動態查詢器部分 中瞭解有關它們的更多資訊

5 跳過 Callbacks

與驗證一樣,也可以使用以下方法跳過 callbacks:

  • decrement!
  • decrement_counter
  • delete
  • delete_all
  • delete_by
  • increment!
  • increment_counter
  • insert
  • insert!
  • insert_all
  • insert_all!
  • touch_all
  • update_column
  • update_columns
  • update_all
  • update_counters
  • upsert
  • upsert_all

但是,這些方法應謹慎使用,因為重要的業務規則和應用程式邏輯可能會儲存在 callbacks 中。在不瞭解潛在影響的情況下繞過它們可能會導致無效資料。

6 停止執行

當您開始為 models 註冊新的 callbacks 時,它們將排隊等待執行。該佇列將包括您模型的所有驗證、註冊的 callbacks 以及要執行的資料庫操作。

整個 callback 鏈都包裹在一個 transaction 中。如果任何 callback 引發異常,則執行鏈將停止併發出 ROLLBACK。有意停止鏈使用:

throw :abort

任何不是 ActiveRecord::RollbackActiveRecord::RecordInvalid 的異常都會在 callback 鏈停止後由 Rails 重新引發。引發除 ActiveRecord::RollbackActiveRecord::RecordInvalid 以外的異常可能會破壞不期望像 saveupdate 之類的方法(通常嘗試返回 truefalse)引發異常的程式碼。

7 關係Callbacks

Callback 透過 model 關係工作,甚至可以由它們定義。假設一個使用者有很多文章的例子。如果使用者被銷燬,使用者的文章應該被銷燬。讓我們透過它與 Article model 的關係為 User model 新增一個 after_destroy 回呼:

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    puts 'Article destroyed'
  end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
Article destroyed
=> #<User id: 1>

8 有條件的 Callbacks

與驗證一樣,我們還可以根據給定謂詞的 satisfaction 來呼叫 callback 方法。我們可以使用 :if:unless 選項來做到這一點,它們可以採用 symbol、ProcArray。當您要指定在何種條件下呼叫 callback 時,您可以使用 :if 選項。如果你想指定在什麼條件下 callback 不應該被呼叫,那麼你可以使用 :unless 選項。

8.1 使用 :if:unlessSymbol

您可以將 :if:unless 選項與 symbol 相關聯,該 symbol 對應於將在 callback 之前呼叫的謂詞方法的名稱。使用:if選項時,如果謂詞方法返回false,則不會執行callback;使用 :unless 選項時,如果謂詞方法返回 true,則不會執行 callback。這是最常見的選項。使用這種註冊形式,還可以註冊幾個不同的謂詞,這些謂詞應該被呼叫以檢查是否應該執行 callback。

class Order < ApplicationRecord
  before_save :normalize_card_number, if: :paid_with_card?
end

8.2 使用 :if:unlessProc

可以將 :if:unlessProc 物件相關聯。此選項最適合編寫簡短的驗證方法,通常是單行程式碼:

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: Proc.new { |order| order.paid_with_card? }
end

由於 proc 是在物件的上下文中計算的,因此也可以將其寫為:

class Order < ApplicationRecord
  before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end

8.3 同時使用 :if 和 :unless

Callback 可以在同一個宣告中混合使用 :if:unless

class Comment < ApplicationRecord
  before_save :filter_content,
    if: Proc.new { forum.parental_control? },
    unless: Proc.new { author.trusted? }
end

8.4 多個 Callback 條件

:if:unless 選項也接受一組過程或方法名稱作為 symbols:

class Comment < ApplicationRecord
  before_save :filter_content,
    if: [:subject_to_parental_control?, :untrusted_author?]
end

callback 僅在所有 :if 條件和 :unless 條件都沒有評估為 true 時執行。

9 Callback 類

有時,您將編寫的 callback 方法非常有用,可以被其他 models 重用。 Active Record 可以建立封裝 callback 方法的類,因此它們可以被重用。

下面是一個示例,我們使用 after_destroy callback 為 PictureFile model 建立一個類:

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

當在類中宣告時,如上所述,callback 方法將接收 model 物件作為引數。我們現在可以在 model 中使用 callback 類:

class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks.new
end

請注意,我們需要實例化一個新的 PictureFileCallbacks 物件,因為我們將回調宣告為實例方法。如果 callbacks 使用實例化物件的狀態,這將特別有用。然而,通常將 callbacks 宣告為類方法會更有意義:

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

如果以這種方式宣告 callback 方法,則不需要實例化 PictureFileCallbacks 物件。

class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks
end

您可以在回呼類中宣告任意數量的 callbacks。

10 Transaction Callbacks

有兩個額外的 callbacks 由資料庫 transaction 的完成觸發:after_commitafter_rollback。這些 callbacks 與 after_save 回呼非常相似,除了它們在資料庫更改提交或回滾後才會執行。當您的 active record models 需要與不屬於資料庫 transaction 的外部系統互動時,它們最有用。

例如,考慮前面的示例,其中 PictureFile model 需要在相應記錄銷燬後刪除檔案。如果在呼叫 after_destroy callback 並且 transaction 回滾後出現任何異常,則該檔案將被刪除並且 model 將處於不一致狀態。例如,假設下面程式碼中的 picture_file_2 無效並且 save! 方法引發錯誤。

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

透過使用 after_commit callback,我們可以解釋這種情況。

class PictureFile < ApplicationRecord
  after_commit :delete_picture_file_from_disk, on: :destroy

  def delete_picture_file_from_disk
    if File.exist?(filepath)
      File.delete(filepath)
    end
  end
end

:on 選項指定何時觸發 callback。如果你 不要提供 :on 選項 callback 將為每個 action 觸發。

由於僅在建立、更新或刪除時使用 after_commit callback 常見的是,這些操作有別名:

class PictureFile < ApplicationRecord
  after_destroy_commit :delete_picture_file_from_disk

  def delete_picture_file_from_disk
    if File.exist?(filepath)
      File.delete(filepath)
    end
  end
end

當 transaction 完成時,為該 transaction 內建立、更新或銷燬的所有 models 呼叫 after_commitafter_rollback callbacks。但是,如果在這些 callbacks 之一中引發異常,則異常將冒泡並且任何剩餘的 after_commitafter_rollback 方法將執行。因此,如果您的回呼程式碼可能引發異常,您需要在回呼中拯救它並處理它,以允許其他 callbacks 執行。

after_commitafter_rollback callbacks 中執行的程式碼本身並不包含在 transaction 中。

使用具有相同方法名稱的 after_create_commitafter_update_commit 將只允許定義的最後一個回呼生效,因為它們都在內部別名為 after_commit,後者覆蓋先前定義的具有相同方法名稱的 callbacks。

class User < ApplicationRecord
  after_create_commit :log_user_saved_to_db
  after_update_commit :log_user_saved_to_db

  private
  def log_user_saved_to_db
    puts 'User was saved to database'
  end
end
irb> @user = User.create # prints nothing

irb> @user.save # updating @user
User was saved to database

還有一個別名用於將 after_commit callback 一起用於建立和更新:

class User < ApplicationRecord
  after_save_commit :log_user_saved_to_db

  private
  def log_user_saved_to_db
    puts 'User was saved to database'
  end
end
irb> @user = User.create # creating a User
User was saved to database

irb> @user.save # updating @user
User was saved to database

回饋

我們鼓勵您幫助提高本指南的品質。

如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。

您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。

如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue

關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區