Active Record 回呼
回呼是 Active Record 物件生命週期中的鉤子,允許您在物件狀態改變之前或之後觸發邏輯。這可以用來確保在呼叫 ActiveRecord::Base#destroy 時刪除關聯和依賴物件(透過覆寫 before_destroy
),或在驗證屬性之前修改它們(透過覆寫 before_validation
)。作為啟動回呼的範例,請考慮針對新紀錄呼叫 ActiveRecord::Base#save
-
(-)
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
此外,可以設定 after_rollback
回呼,以便在發出回滾時觸發。查看 ActiveRecord::Transactions
以取得更多關於 after_commit
和 after_rollback
的詳細資訊。
此外,每當物件被觸及時,都會觸發 after_touch
回呼。
最後,對於搜尋器找到和實例化的每個物件,都會觸發 after_find
和 after_initialize
回呼,其中 after_initialize
也會在新物件被實例化後觸發。
總共有十九個回呼,它們提供了對如何在 Active Record 生命週期的每個狀態做出反應和準備的大量控制。呼叫現有記錄的 ActiveRecord::Base#save 的順序類似,只是每個 _create
回呼都被相應的 _update
回呼取代。
範例
class CreditCard < ActiveRecord::Base
# Strip everything but digits, so the user can specify "555 234 34" or
# "5552-3434" and both will mean "55523434"
before_validation(on: :create) do
self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
end
end
class Subscription < ActiveRecord::Base
before_create :record_signup
private
def record_signup
self.signed_up_on = Date.today
end
end
class Firm < ActiveRecord::Base
# Disables access to the system, for associated clients and people when the firm is destroyed
before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') }
before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') }
end
可繼承的回呼佇列
除了可覆寫的回呼方法之外,還可以透過使用回呼巨集來註冊回呼。它們的主要優點是巨集將行為添加到透過繼承階層保持完整的回呼佇列中。
class Topic < ActiveRecord::Base
before_destroy :destroy_author
end
class Reply < Topic
before_destroy :destroy_readers
end
當執行 Topic#destroy
時,只會呼叫 destroy_author
。當執行 Reply#destroy
時,會同時呼叫 destroy_author
和 destroy_readers
。
**重要:**為了讓繼承適用於回呼佇列,您必須在指定關聯之前指定回呼。否則,您可能會在父級註冊回呼之前觸發子級的載入,並且它們將不會被繼承。
回呼的類型
回呼巨集接受三種類型的回呼:方法參考(符號)、回呼物件、內聯方法(使用 proc)。方法參考和回呼物件是推薦的方法,使用 proc 的內聯方法有時是合適的(例如用於創建 mix-in)。
方法參考回呼透過指定物件中可用的受保護或私有方法來工作,如下所示
class Topic < ActiveRecord::Base
before_destroy :delete_parents
private
def delete_parents
self.class.delete_by(parent_id: id)
end
end
回呼物件具有以回呼命名的方法,並以記錄作為唯一參數,例如
class BankAccount < ActiveRecord::Base
before_save EncryptionWrapper.new
after_save EncryptionWrapper.new
after_initialize EncryptionWrapper.new
end
class EncryptionWrapper
def before_save(record)
record.credit_card_number = encrypt(record.credit_card_number)
end
def after_save(record)
record.credit_card_number = decrypt(record.credit_card_number)
end
alias_method :after_initialize, :after_save
private
def encrypt(value)
# Secrecy is committed
end
def decrypt(value)
# Secrecy is unveiled
end
end
因此,您可以指定要在給定回呼上傳送訊息的物件。當該回呼被觸發時,該物件具有一個以回呼訊息命名的方法。您可以透過傳入其他初始化數據(例如要使用的屬性的名稱)來使這些回呼更具彈性
class BankAccount < ActiveRecord::Base
before_save EncryptionWrapper.new("credit_card_number")
after_save EncryptionWrapper.new("credit_card_number")
after_initialize EncryptionWrapper.new("credit_card_number")
end
class EncryptionWrapper
def initialize(attribute)
@attribute = attribute
end
def before_save(record)
record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
end
def after_save(record)
record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
end
alias_method :after_initialize, :after_save
private
def encrypt(value)
# Secrecy is committed
end
def decrypt(value)
# Secrecy is unveiled
end
end
before_validation*
返回語句
如果 before_validation
回呼拋出 :abort
,則該過程將被中止,並且 ActiveRecord::Base#save 將返回 false
。如果呼叫 ActiveRecord::Base#save!,它將引發 ActiveRecord::RecordInvalid
異常。錯誤物件不會被附加任何內容。
取消回呼
如果 before_*
回呼拋出 :abort
,則所有後續回呼和關聯的操作都將被取消。回呼通常按照它們定義的順序運行,但定義為模型方法的回呼除外,它們最後被呼叫。
排序回呼
有時應用程式程式碼要求回呼以特定順序執行。例如,before_destroy
回呼(在本例中為 log_children
)應在 children
關聯中的記錄被 dependent: :destroy
選項銷毀之前執行。
讓我們看看下面的程式碼
class Topic < ActiveRecord::Base
has_many :children, dependent: :destroy
before_destroy :log_children
private
def log_children
# Child processing
end
end
在這種情況下,問題是當執行 before_destroy
回呼時,children
關聯中的記錄不再存在,因為 ActiveRecord::Base#destroy 回呼首先被執行。您可以在 before_destroy
回呼上使用 prepend
選項來避免這種情況。
class Topic < ActiveRecord::Base
has_many :children, dependent: :destroy
before_destroy :log_children, prepend: true
private
def log_children
# Child processing
end
end
這樣,before_destroy
會在呼叫 dependent: :destroy
之前執行,並且數據仍然可用。
此外,有些情況下您希望按順序執行多個相同類型的回呼。
例如
class Topic < ActiveRecord::Base
has_many :children
after_save :log_children
after_save :do_something_else
private
def log_children
# Child processing
end
def do_something_else
# Something else
end
end
在這種情況下,log_children
在 do_something_else
之前執行。這適用於所有非交易回呼,以及 before_commit
。
對於交易 after_
回呼(after_commit
、after_rollback
等),可以透過設定來設定順序。
config.active_record.run_after_transaction_callbacks_in_order_defined = false
當設定為 true
(Rails 7.1 的預設值)時,回呼會按照它們定義的順序執行,就像上面的例子一樣。當設定為 false
時,順序會反轉,因此 do_something_else
會在 log_children
之前執行。
交易
#save、#save! 或 #destroy 呼叫的整個回呼鏈都在一個交易中運行。這包括 after_*
鉤子。如果一切順利,則在鏈完成後執行 COMMIT
。
如果 before_*
回呼取消了動作,則會發出 ROLLBACK
。您也可以在任何回呼(包括 after_*
鉤子)中引發異常來觸發 ROLLBACK
。但是請注意,在這種情況下,客戶端需要意識到這一點,因為普通的 #save 將引發此類異常,而不是靜默地返回 false
。
除錯回呼
可以透過物件上的 _*_callbacks
方法訪問回呼鏈。Active Model Callbacks 支援 :before
、:after
和 :around
作為 kind
屬性的值。kind
屬性定義了回呼在鏈的哪個部分運行。
要查找 before_save
回呼鏈中的所有回呼
Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
返回組成 before_save
鏈的回呼物件陣列。
要進一步檢查 before_save 鏈是否包含定義為 rest_when_dead
的 proc,請使用回呼物件的 filter
屬性
Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
根據 Topic 模型上的 before_save
回呼鏈中是否包含 proc 返回 true 或 false。
常數
CALLBACKS(回呼) | = | [ :after_initialize, :after_find, :after_touch, :before_validation, :after_validation, :before_save, :around_save, :after_save, :before_create, :around_create, :after_create, :before_update, :around_update, :after_update, :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] |