如何在before_回调中触发state-machines的显式状态机事件?
如何在before_回调中触发state-machines状态机事件?
我有一个使用state-machines gem的ActiveRecord模型,希望在before_validation或before_save回调中触发状态机事件,代码如下:
class Model < ApplicationRecord state_machine initial: :pending do event :complete do transition pending: :complete, if: :some_guard_clause? end event :fail do transition pending: :failed, if: :some_other_guard_clause? end end before_validation :attempt_transitions private def attempt_transitions self.state_event = "complete" if can_complete? self.state_event = "fail" if can_fail? end end
但遇到以下问题:
- 在
before_回调中设置self.state_event无效果,已确认方法被调用且赋值正确,但保存时状态未变更,推测是因为state-machines仅在其around过滤器开始时检查该值,而此时已过检查时机。 - 直接设置
Model#state=不会触发状态机的before_transition和after_transition回调,而我依赖这些回调。 - 使用
#complete/#complete!触发更新会导致无限递归,这是因为在保存操作内再次触发保存导致的。 - 无法改用
after_save回调,因为这会导致两次UPDATE操作及重复的副作用(如其他模型的回调)。
解决方案
直接调用状态机的transition方法手动触发状态变更,该方法会执行所有状态机回调,但不会触发额外的保存操作,状态变更会被纳入当前的保存流程中:
class Model < ApplicationRecord state_machine initial: :pending do event :complete do transition pending: :complete, if: :some_guard_clause? end event :fail do transition pending: :failed, if: :some_other_guard_clause? end # 示例状态回调,验证触发逻辑 before_transition on: :complete do |model| model.some_callback_method end end before_validation :attempt_transitions private def attempt_transitions # 优先检查complete条件,满足则执行状态转换 if can_complete? state_machine.transition(:complete) elsif can_fail? state_machine.transition(:fail) end end end
方案说明
state_machine.transition(:event_name)会执行状态机的完整逻辑:检查守卫条件、触发before_transition和after_transition回调,仅修改内存中的状态值,不会主动调用save,因此状态变更会被包含在当前的保存流程里,只产生一次数据库UPDATE操作。- 使用
elsif确保同一时间只触发一个状态事件,避免逻辑冲突。 - 该方式完美解决所有问题:状态变更生效、触发全部回调、无递归问题、仅单次数据库操作。
内容的提问来源于stack exchange,提问作者janfoeh
相关产品推荐
相关产品推荐

