ActiveRecord 定義了 20 個回調,覆蓋對象從誕生到銷毀的全過程:
before_* / after_* / around_*after_find / after_initialize(無 before 對應)
# 完整鏈 before_validation → after_validation before_save → around_save → after_save before_create → around_create → after_create after_commit / after_rollback(事務提交/回滾後) # 更新時替換 create 為 update # 銷毀時替換為 destroy
class Order < ActiveRecord::Base
def after_save
self.payment_due ||= Time.now + 30.days
end
end
class Order < ActiveRecord::Base
before_validation :normalize_credit_card_number
after_create { |order| logger.info "Order #{order.id} created" }
after_update :send_confirmation_email, if: :shipped?
protected
def normalize_credit_card_number
self.cc_number.gsub!(/[-\s]/, '')
end
def shipped?
status == 'shipped'
end
end
false 中斷鏈(before_* 還會阻止後續動作)。around_* 接收 block,需手動 yield 執行核心動作。為性能考慮,這兩個回調只能定義為實例方法,聲明式方式會被忽略。且必須在類中顯式定義(哪怕是空方法)才會觸發:
class Order < ActiveRecord::Base
after_find :log_access
def after_find # 必須聲明,哪怕為空
# 這裡可以不寫邏輯,僅為激活回調機制
end
private
def log_access
logger.debug "Order #{id} loaded"
end
end
class CreditCardCallbacks
def before_validation(model)
model.cc_number.gsub!(/[-\s]/, '')
end
end
class Order < ActiveRecord::Base
before_validation CreditCardCallbacks.new
end
class Subscription < ActiveRecord::Base
before_validation CreditCardCallbacks.new
end
處理器類只需實現對應回調方法名,即可跨模型復用。要求模型擁有同名屬性(cc_number)。
class Encrypter
def initialize(attrs)
@attrs = attrs
end
def before_save(model)
@attrs.each { |f| model[f].tr!('a-zA-Z', 'b-zaB-ZA') }
end
def after_save(model)
@attrs.each { |f| model[f].tr!('b-zaB-ZA', 'a-zA-Z') }
end
alias_method :after_find, :after_save
end
class Order < ActiveRecord::Base
encrypter = Encrypter.new([:name, :email])
before_save encrypter
after_save encrypter
after_find encrypter
def after_find; end # 激活 after_find
end
class ActiveRecord::Base
def self.encrypt(*attrs)
encrypter = Encrypter.new(attrs)
before_save encrypter
after_save encrypter
after_find encrypter
define_method(:after_find) { }
end
end
class Order < ActiveRecord::Base
encrypt :name, :email
end
回調易導致模型承擔過多副作用(發郵件、寫日志、清緩存、觸發 Webhook)。Observer 透明注入行為,零修改模型代碼。
class OrderObserver < ActiveRecord::Observer
def after_save(order)
order.logger.info "Order #{order.id} created"
end
end
# 約定:OrderObserver 自動觀察 Order 模型
class AuditObserver < ActiveRecord::Observer
observe Order, Payment, Refund
def after_save(model)
model.logger.info "[Audit] #{model.class} #{model.id} created"
end
end
# config/application.rb config.active_record.observers = :order_observer, :audit_observer # 或單獨應用中手動實例化 OrderObserver.instance AuditObserver.instance
app/models 目錄。| 場景 | 推薦 | 理由 |
|---|---|---|
| 數據標準化/格式化/衍生屬性計算 | 回調 | 屬於模型核心不變量,內聚性高 |
| 發送通知郵件/推送 | Observer / Service Object | 副作用、可失敗、需重試、屬於用例編排 |
| 審計日志/安全記錄 | Observer | 橫切所有模型,透明無侵入 |
| 緩存失效/搜索索引更新 | Observer / after_commit | 需在事務提交後執行,避免髒讀 |
| 第三方系統同步 | Service Object + 異步隊列 | 耦合外部依賴,不屬於模型職責 |
after_save { UserMailer.welcome(self).deliver_later } 改用異步。User.import(records, validate: false, callbacks: false)(需 activerecord-import gem)。expect(order).to receive(:normalize_credit_card_number)。ActiveRecord 回調提供了強大的生命週期介入能力,但「大權在手,責任在肩」:
after_find/after_initialize 必須定義實例方法激活。after_commit 確保事務一致性邊界。「回調是模型的內部事務,Observer 是系統的橫切關注點。」劃清界限,方得始終。