1 物件生命週期
在 Rails 應用程式的正常運作期間,物件可能會建立、更新和銷毀。Active Record 提供了這個 物件生命週期 的掛鉤,以便您可以控制您的應用程式及其資料。
回呼讓您可以在物件狀態變更之前或之後觸發邏輯。
class Baby < ApplicationRecord
after_create -> { puts "Congratulations!" }
end
irb> @baby = Baby.create
Congratulations!
正如您所見,有許多生命週期事件,您可以選擇在這些事件之前、之後,甚至是在它們周圍進行掛鉤。
2 回呼概觀
回呼是在物件生命週期的特定時刻被呼叫的方法。透過回呼,可以撰寫在每次 Active Record 物件從資料庫建立、儲存、更新、刪除、驗證或載入時執行的程式碼。
2.1 回呼註冊
為了使用可用的回呼,您需要註冊它們。您可以將回呼實作為一般方法,並使用巨集樣式的類別方法將它們註冊為回呼
class User < ApplicationRecord
validates :login, :email, presence: true
before_validation :ensure_login_has_a_value
private
def ensure_login_has_a_value
if login.blank?
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
或者,您可以將一個程序傳遞給要觸發的回呼。
class User < ApplicationRecord
before_create ->(user) { user.name = user.login.capitalize if user.name.blank? }
end
最後,您可以定義您自己的自訂回呼物件,我們將在 下方的後續內容中更詳細地說明。
class User < ApplicationRecord
before_create MaybeAddName
end
class MaybeAddName
def self.before_create(record)
if record.name.blank?
record.name = record.login.capitalize
end
end
end
回呼也可以註冊為僅在特定生命週期事件中觸發,這允許完全控制您的回呼觸發的時間和背景。
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
將回呼方法宣告為私有方法被認為是良好的做法。如果保留為公開方法,它們可以從模型外部呼叫,並違反物件封裝的原則。
避免在回呼中呼叫會對物件產生副作用的 update
、save
或其他方法。例如,不要在回呼中呼叫 update(attribute: "value")
。這可能會改變模型的狀態,並可能在提交期間造成意外的副作用。相反地,你可以在 before_create
/ before_update
或更早的回呼中安全地直接指定值(例如,self.attribute = "value"
)。
3 個可用的回呼
以下是所有可用的 Active Record 回呼清單,列出的順序與它們在各個操作期間被呼叫的順序相同
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
after_save
會在建立和更新時執行,但總是 在 更具體的回呼 after_create
和 after_update
之後 執行,不論巨集呼叫執行的順序為何。
3.3 銷毀物件
before_destroy
回呼應該置於 dependent: :destroy
關聯之前(或使用 prepend: true
選項),以確保它們在記錄被 dependent: :destroy
刪除之前執行。
after_commit
所提供的保證與 after_save
、after_update
和 after_destroy
大不相同。例如,如果在 after_save
中發生例外狀況,交易將會回滾,資料也不會持續存在。而發生在 after_commit
中的任何事都可以保證交易已完成,且資料已持續存在資料庫中。有關 交易回呼 的更多資訊如下。
3.4 after_initialize
和 after_find
每當建立 Active Record 物件時,after_initialize
回呼就會被呼叫,無論是直接使用 new
或從資料庫載入記錄時。這有助於避免直接覆寫 Active Record 的 initialize
方法。
從資料庫載入記錄時,after_find
回呼就會被呼叫。如果兩個回呼都已定義,after_find
會在 after_initialize
之前被呼叫。
after_initialize
和 after_find
回呼沒有 before_*
對應項。
它們可以像其他 Active Record 回呼一樣註冊。
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
回呼就會被呼叫。
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 Book < ApplicationRecord
belongs_to :library, touch: true
after_touch do
puts 'A Book was touched'
end
end
class Library < ApplicationRecord
has_many :books
after_touch :log_when_books_or_library_touched
private
def log_when_books_or_library_touched
puts 'Book/Library was touched'
end
end
irb> @book = Book.last
=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
irb> @book.touch # triggers @book.library.touch
A Book was touched
Book/Library was touched
=> true
4 執行回呼
下列方法會觸發回呼
create
create!
destroy
destroy!
destroy_all
destroy_by
save
save!
save(validate: false)
save!(validate: false)
切換!
觸摸
更新_屬性
更新
更新!
有效?
此外,after_find
回呼會由下列尋找器方法觸發
全部
第一個
尋找
依據尋找
依據_*尋找
依據_*!尋找
依據_sql尋找
最後一個
每次類別的新物件初始化時,after_initialize
回呼都會觸發。
find_by_*
和 find_by_*!
方法是動態尋找器,會自動為每個屬性產生。在 動態尋找器區段 了解更多關於它們的資訊
5 略過回呼
就像驗證一樣,也可以使用下列方法略過回呼
遞減!
遞減計數器
刪除
全部刪除
依據刪除
遞增!
遞增計數器
插入
插入!
全部插入
全部插入!
全部觸摸
更新欄位
更新欄位
全部更新
更新計數器
更新或插入
全部更新或插入
不過,這些方法應謹慎使用,因為重要的商業規則和應用程式邏輯可能會保留在回呼中。在不了解潛在影響的情況下繞過它們可能會導致無效的資料。
6 停止執行
當您開始為模型註冊新的回呼時,它們會排隊執行。此佇列將包含您所有模型的驗證、註冊的回呼和要執行的資料庫操作。
整個回呼鏈會包裝在交易中。如果任何回呼引發例外,執行鏈會停止,並發出 ROLLBACK。若要故意停止鏈,請使用
throw :abort
任何不是 ActiveRecord::Rollback
或 ActiveRecord::RecordInvalid
的例外,都會在呼叫回鏈停止後由 Rails 重新引發。此外,可能會中斷不預期方法(例如通常嘗試傳回 true
或 false
的 save
和 update
)引發例外的程式碼。
如果在 after_destroy
、before_destroy
或 around_destroy
呼叫回中引發 ActiveRecord::RecordNotDestroyed
,則不會重新引發,而 destroy
方法將傳回 false
。
7 關聯呼叫回
呼叫回透過模型關聯運作,甚至可以由它們定義。假設一個範例,一個使用者有許多文章。如果使用者被刪除,使用者的文章應該被刪除。讓我們透過與 Article
模型的關聯,新增一個 after_destroy
呼叫回至 User
模型
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 關聯呼叫回
關聯呼叫回類似於一般呼叫回,但它們是由集合生命週期中的事件觸發的。有四個可用的關聯呼叫回
before_add
after_add
before_remove
after_remove
您可以透過新增選項至關聯宣告來定義關聯呼叫回。例如
class Author < ApplicationRecord
has_many :books, before_add: :check_credit_limit
def check_credit_limit(book)
# ...
end
end
Rails 將正在新增或移除的物件傳遞給呼叫回。
您可以透過將呼叫回作為陣列傳遞,將它們堆疊在單一事件上
class Author < ApplicationRecord
has_many :books,
before_add: [:check_credit_limit, :calculate_shipping_charges]
def check_credit_limit(book)
# ...
end
def calculate_shipping_charges(book)
# ...
end
end
如果 before_add
呼叫回擲回 :abort
,則物件不會被新增至集合。類似地,如果 before_remove
呼叫回擲回 :abort
,則物件不會從集合中移除
# book won't be added if the limit has been reached
def check_credit_limit(book)
throw(:abort) if limit_reached?
end
這些呼叫回僅在透過關聯集合新增或移除關聯物件時才會被呼叫
# Triggers `before_add` callback
author.books << book
author.books = [book, book2]
# Does not trigger the `before_add` callback
book.update(author_id: 1)
9 條件呼叫回
與驗證相同,我們也可以讓呼叫回呼函數的方法有條件地滿足給定的謂詞。我們可以使用 :if
和 :unless
選項來做到這一點,它們可以接受符號、Proc
或 Array
。
當您想要指定在哪些條件下應該呼叫回呼時,可以使用 :if
選項。如果您想要指定在哪些條件下不應該呼叫回呼,則可以使用 :unless
選項。
9.1 使用 :if
和 :unless
與 Symbol
您可以將 :if
和 :unless
選項與對應於謂詞方法名稱的符號關聯起來,該謂詞方法將在回呼之前立即被呼叫。
在使用 :if
選項時,如果謂詞方法傳回 false,則不會執行回呼;在使用 :unless
選項時,如果謂詞方法傳回 true,則不會執行回呼。這是最常見的選項。
class Order < ApplicationRecord
before_save :normalize_card_number, if: :paid_with_card?
end
使用這種註冊表單,也可以註冊幾個不同的謂詞,這些謂詞應該被呼叫來檢查是否應該執行回呼。我們將在下方介紹這一點 。
9.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
9.3 多個回呼條件
:if
和 :unless
選項也接受 proc 或方法名稱的陣列作為符號
class Comment < ApplicationRecord
before_save :filter_content,
if: [:subject_to_parental_control?, :untrusted_author?]
end
您可以在條件清單中輕鬆地包含 proc
class Comment < ApplicationRecord
before_save :filter_content,
if: [:subject_to_parental_control?, Proc.new { untrusted_author? }]
end
9.4 同時使用 :if
和 :unless
回呼函式可以在同一個宣告中混合 :if
和 :unless
class Comment < ApplicationRecord
before_save :filter_content,
if: Proc.new { forum.parental_control? },
unless: Proc.new { author.trusted? }
end
回呼函式只會在所有 :if
條件都成立且沒有任何 :unless
條件評估為 true
時執行。
10 回呼函式類別
有時你寫的回呼函式方法對其他模型來說夠有幫助,可以重複使用。Active Record 讓你可以建立封裝回呼函式方法的類別,以便重複使用。
以下是我們建立一個類別的範例,其中包含一個 after_destroy
回呼函式,用來處理檔案系統中已捨棄檔案的清除。這種行為可能不只我們 PictureFile
模型獨有,我們可能想要分享它,因此將它封裝到一個獨立的類別中會是個好主意。這會讓測試這種行為和變更它變得容易許多。
class FileDestroyerCallback
def after_destroy(file)
if File.exist?(file.filepath)
File.delete(file.filepath)
end
end
end
當在類別內宣告,如同上述,回呼函式方法會收到模型物件作為一個參數。這會在任何使用該類別的模型上執行,如下所示
class PictureFile < ApplicationRecord
after_destroy FileDestroyerCallback.new
end
請注意我們需要實例化一個新的 FileDestroyerCallback
物件,因為我們將我們的回呼函式宣告為一個實例方法。如果回呼函式使用實例化物件的狀態,這會特別有用。然而,通常將回呼函式宣告為類別方法會比較有意義
class FileDestroyerCallback
def self.after_destroy(file)
if File.exist?(file.filepath)
File.delete(file.filepath)
end
end
end
當回呼函式方法以這種方式宣告時,將不需要在我們的模型中實例化一個新的 FileDestroyerCallback
物件。
class PictureFile < ApplicationRecord
after_destroy FileDestroyerCallback
end
你可以在回呼函式類別中宣告任意數量的回呼函式。
11 交易回呼函式
11.1 after_commit
和 after_rollback
資料庫交易完成時會觸發兩個附加的回呼:after_commit
和 after_rollback
。這些回呼與 after_save
回呼非常類似,只是在資料庫變更提交或回滾後才會執行。當 Active Record 模型需要與不屬於資料庫交易的外部系統互動時,它們最為有用。
例如,考慮先前的範例,其中 PictureFile
模型需要在對應的記錄被刪除後刪除檔案。如果在呼叫 after_destroy
回呼後有任何錯誤導致交易回滾,檔案將會被刪除,而模型將會處於不一致的狀態。例如,假設以下程式碼中的 picture_file_2
無效,而 save!
方法引發錯誤。
PictureFile.transaction do
picture_file_1.destroy
picture_file_2.save!
end
透過使用 after_commit
回呼,我們可以考量這種情況。
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
選項指定何時觸發回呼。如果您未提供 :on
選項,回呼將會對每個動作觸發。
當交易完成時,將會針對在該交易中建立、更新或刪除的所有模型呼叫 after_commit
或 after_rollback
回呼。不過,如果在其中一個回呼中引發例外狀況,例外狀況將會浮現,而任何剩餘的 after_commit
或 after_rollback
方法將不會執行。因此,如果您的回呼程式碼可能會引發例外狀況,您需要在回呼中救援並處理它,以允許其他回呼執行。
在 after_commit
或 after_rollback
回呼中執行的程式碼本身並未包含在交易中。
在單一交易的背景下,如果你與多個載入的物件互動,這些物件代表資料庫中的同一個記錄,在 after_commit
和 after_rollback
回呼中有一個關鍵行為需要注意。這些回呼只會觸發在交易中經歷變更的特定記錄的第一個物件。其他載入的物件,儘管代表同一個資料庫記錄,它們各自的 after_commit
或 after_rollback
回呼不會被觸發。這種細微的行為在以下情況中特別有影響力:你預期與同一個資料庫記錄相關聯的每個物件都能獨立執行回呼。它會影響回呼順序的流程和可預測性,導致交易後應用程式邏輯出現潛在的不一致性。
11.2 after_commit
的別名
由於只在建立、更新或刪除時使用 after_commit
回呼很常見,因此這些操作有一些別名
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
將 after_create_commit
和 after_update_commit
與同一個方法名稱一起使用,只會讓最後定義的回呼生效,因為它們在內部都別名為 after_commit
,它會覆寫先前定義的、具有相同方法名稱的回呼。
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
11.3 after_save_commit
還有一個 after_save_commit
,它是同時使用 after_commit
回呼建立和更新的別名
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
11.4 交易回呼排序
預設情況下,回呼會按照它們被定義的順序執行。然而,在定義多個交易 after_
回呼(after_commit
、after_rollback
等)時,順序可能會與它們被定義時的順序相反。
class User < ActiveRecord::Base
after_commit { puts("this actually gets called second") }
after_commit { puts("this actually gets called first") }
end
這也適用於所有 after_*_commit
變體,例如 after_destroy_commit
。
此順序可透過設定檔設定
config.active_record.run_after_transaction_callbacks_in_order_defined = false
設定為 true
(Rails 7.1 的預設值)時,會按定義順序執行 callback。設定為 false
時,順序會反轉,就像上面的範例一樣。
回饋
我們鼓勵您協助提升本指南的品質。
如果您發現任何錯字或事實錯誤,請協助我們修正。您可以閱讀我們的 文件貢獻 部分,了解如何開始。
您也可能會發現不完整或過時的內容。請務必為 main 新增任何遺漏的文件。請先查看 Edge Guides,確認問題是否已在 main 分支中修正。查看 Ruby on Rails Guides Guidelines,了解風格和慣例。
如果您發現需要修正的地方,但無法自行修改,請 開啟問題。
最後,我們歡迎您在 官方 Ruby on Rails 論壇 討論任何與 Ruby on Rails 文件相關的主題。