最近遇到了這麼個需求,需要針對狀態機內的 event 有的部分要 valiadte,有的部分則不需要,但是所有的 event 卻都要有 ActiveRecord 的 callback
需求釐清
- 商品上架需要通過所有的 validate, 這些是商業邏輯的規則
- 商品下架不需要通過,因為可能有市場價格浮動,導致 validate 有 fail 的可能
- 每一個 event 都要有 PaperTrail 的紀錄
第一步:Skip 所有 validate + callback
原先我以為 AASM 可以直接做到 skip 指定 event 的 validate,但文件東翻西找,卻只有
If you want make sure the state gets saved without running validations (and thereby maybe persisting an invalid object state), simply tell AASM to skip the validations. Be aware that when skipping validations, only the state column will be updated in the database (just like ActiveRecord update_column is working).
這個如果設定為 true, 等同於將所有 event!(加驚嘆號)變成 update_column, 也就是單純更新欄位,不執行任何 callback
也就是說,你只能選擇全要或是全不要。
aasm skip_validation_on_save: true do
event :go_live do
transitions from: :offline, to: :online
end
event :go_offline do
transitions from: :online, to: :offline
end
end
第二步:針對個別 Validate
其實 ActiveRecord 中可以直接使用 valid?
這個 method 去執行 validate
所以我們可以讓商品上架的這個行為,單獨使用 guard 去檢查 valid?
event :go_live do
transitions from: :offline, to: :online, guard: [:valid?]
end
這樣子就可以做到, go_live 會被 validate 而 go_offline 不會。
第三步:讓所有 event 都有 callback
我爬了 AASM 的文件,發現有一個 method 可以在所有 event 執行後執行,也就是 after_commit 的概念
after_all_events
不過在官方文件中沒有詳細說明這個 method, 自己試了之後發現,確實是會在所有 event 執行後進行 commit
那我們就可以設計一個執行 update callback 來讓 PaperTrail 產生記錄
aasm skip_validation_on_save: true do
after_all_events :trigger_update_callback
event :go_live do
transitions from: :offline, to: :online
end
event :go_offline do
transitions from: :online, to: :offline
end
end
private
def trigger_update_callback
touch(:updated_at) && run_callbacks(:update) if persisted?
end
這邊解釋一下 touch(:updated_at)
,是因為我發現 PaperTrail 在生成 Log 時的 created_at 並不是真正的數據建立時間,而是拿 object 的 updated_at 時間,我可以在 PaperTrail 中的 record_trail.rb 看見
所以這裡順序很重要,一定是先執行 touch(:updated_at)
,再執行 run_callbacks(:update)
這樣 PaperTrail 裡面的時間才會是對的。
而後面的 .persisted?
也是非加不可,如果遇到數據是新建立的,沒有這個判斷就會噴
ActiveRecord::RecordInvalid: Validation failed: Children parent must exist
結論
這個問題我花了好長一段時間才想到解法的,可以兼顧可讀性、又能夠完美達成刁鑽的需求,應該此生難忘了XD