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 建立物件
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
-
after_commit
/after_rollback
3.2 更新物件
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
-
after_commit
/after_rollback
3.3 銷燬物件
after_save
在建立和更新時執行,但總是after 更具體的 callbacks after_create
和 after_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_initialize
和 after_find
after_initialize
callback 將在實例化 Active Record 物件時呼叫,直接使用 new
或從資料庫載入記錄時。避免直接覆蓋 Active Record initialize
方法的需要會很有用。
每當 Active Record 從資料庫載入記錄時,都會呼叫 after_find
callback。如果兩者都定義了,則 after_find
在 after_initialize
之前呼叫。
after_initialize
和 after_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::Rollback
或 ActiveRecord::RecordInvalid
的異常都會在 callback 鏈停止後由 Rails 重新引發。引發除 ActiveRecord::Rollback
或 ActiveRecord::RecordInvalid
以外的異常可能會破壞不期望像 save
和 update
之類的方法(通常嘗試返回 true
或 false
)引發異常的程式碼。
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、Proc
或 Array
。當您要指定在何種條件下呼叫 callback 應時,您可以使用 :if
選項。如果你想指定在什麼條件下 callback 不應該被呼叫,那麼你可以使用 :unless
選項。
8.1 使用 :if
和 :unless
和 Symbol
您可以將 :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
和 :unless
和 Proc
可以將 :if
和 :unless
與 Proc
物件相關聯。此選項最適合編寫簡短的驗證方法,通常是單行程式碼:
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_commit
和 after_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_commit
或 after_rollback
callbacks。但是,如果在這些 callbacks 之一中引發異常,則異常將冒泡並且任何剩餘的 after_commit
或 after_rollback
方法將不執行。因此,如果您的回呼程式碼可能引發異常,您需要在回呼中拯救它並處理它,以允許其他 callbacks 執行。
在 after_commit
或 after_rollback
callbacks 中執行的程式碼本身並不包含在 transaction 中。
使用具有相同方法名稱的 after_create_commit
和 after_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 討論區。