Nic Lin's Blog

喜歡在地上滾的工程師

[Rails] 如何讓 AASM 的 event 事件個別 validate 且有 callback

最近遇到了這麼個需求,需要針對狀態機內的 event 有的部分要 valiadte,有的部分則不需要,但是所有的 event 卻都要有 ActiveRecord 的 callback

需求釐清

  • 商品上架需要通過所有的 validate, 這些是商業邏輯的規則
  • 商品下架不需要通過,因為可能有市場價格浮動,導致 validate 有 fail 的可能
  • 每一個 event 都要有 PaperTrail 的紀錄

第一步:Skip 所有 validate + callback

原先我以為 AASM 可以直接做到 skip 指定 event 的 validate,但文件東翻西找,卻只有

skip_validation_on_save

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

comments powered by Disqus