【问题标题】:Validation before persistance on state_machine gem在 state_machine gem 上持久化之前的验证
【发布时间】:2013-12-08 02:08:14
【问题描述】:

state_machine gem 中的转换之前执行验证的正确语法是什么?

我尝试了以下方法,

before_transition :apple => :orange do
  validate :validate_core
end

def validate_core
  if core.things.blank?
    errors.add(:core, 'must have one thing')
  end
end

但我收到以下错误,

undefined method `validate' for #<StateMachine::Machine:0x007ffed73e0bd8>

我也试过写成,

state :orange do
  validate :validate_core
end

但这会导致记录保存后回滚,不太理想。我想首先阻止状态机转换为:orange

核心问题是在我的控制器中,我的逻辑依赖于object.save 的结果。我对状态机的验证直到初始保存后才会启动,因此保存返回为 true,并且控制器继续执行逻辑,如果对象无效,它不应该命中。

除了检查保存之外,我还通过手动测试有效性来解决这个问题,但感觉应该有一种方法可以在对象保存之前触发验证。

【问题讨论】:

    标签: ruby-on-rails ruby validation ruby-on-rails-4 state-machine


    【解决方案1】:

    特定状态机的想法是在状态中嵌入验证声明。

    state :orange do
      validate :validate_core
    end
    

    只要对象转换为橙色,上述配置就会执行验证:validate_core

    event :orangify do
      transition all => :orange
    end
    

    我了解您对回滚的担忧,但请记住,回滚是在事务中执行的,因此非常便宜。

    record.orangify!
    

    此外,请记住,您还可以使用不使用异常的非 bang 版本。

    > c.orangify
       (0.3ms)  BEGIN
       (0.3ms)  ROLLBACK
     => false 
    

    也就是说,如果你想基于之前的转换使用不同的方法,那么你只需要知道如果回调返回 false,转换就会停止。

    before_transition do
      false
    end
    
    > c.orangify!
       (0.2ms)  BEGIN
       (0.2ms)  ROLLBACK
    StateMachine::InvalidTransition: Cannot transition state via :cancel from :purchased (Reason(s): Transition halted)
    

    请注意,事务始终会启动,但如果回调位于最开始,则可能不会执行任何查询。

    before_transaction 接受一些参数。您可以生成对象和事务实例。

    before_transition do |object, transaction|
      object.validate_core
    end
    

    事实上你可以通过事件来限制它

    before_transition all => :orange do |object, transaction|
      object.validate_core # => false
    end
    

    在这种情况下,validate_core 应该是一个返回 true/false 的简单方法。如果你想使用定义的验证链,那么我想到的是在模型本身上调用valid?

    before_transition all => :orange do |object, transaction|
      object.valid?
    end
    

    但是,请注意,您不能在事务范围之外运行事务。事实上,如果您检查perform 的代码,您会看到回调在事务内部。

    # Runs each of the collection's transitions in parallel.
    # 
    # All transitions will run through the following steps:
    # 1. Before callbacks
    # 2. Persist state
    # 3. Invoke action
    # 4. After callbacks (if configured)
    # 5. Rollback (if action is unsuccessful)
    # 
    # If a block is passed to this method, that block will be called instead
    # of invoking each transition's action.
    def perform(&block)
      reset
    
      if valid?
        if use_event_attributes? && !block_given?
          each do |transition|
            transition.transient = true
            transition.machine.write(object, :event_transition, transition)
          end
    
          run_actions
        else
          within_transaction do
            catch(:halt) { run_callbacks(&block) }
            rollback unless success?
          end
        end
      end
    
      # ...
    end
    

    要跳过事务,您应该对 state_machine 进行猴子修补,以便转换方法(例如 orangify!)在转换之前检查记录是否有效。

    以下是您应该实现的目标的示例

    # Override orangify! state machine action
    # If the record is valid, then perform the actual transition,
    # otherwise return early.
    def orangify!(*args)
      return false unless self.valid?
      super
    end
    

    当然,您不能为每个方法手动执行此操作,这就是为什么您应该修改库以实现此结果。

    【讨论】:

    • 问题不在于回滚的性能,更多的是保存过程(至少最初是这样)。因此,在我的为向导提供动力的控制器中,控制器检查保存,然后继续并将用户带到下一步。同时,后台状态回滚,用户现在处于他们不应该访问的步骤。我在我的控制器中有一个明确的检查,但感觉像是不必要的混乱。
    • 您是否尝试过返回 false,如我所示?它应该停止过渡。
    • 我做到了,而且确实做到了,但它仍然将其作为回滚。我将更新我的问题以更好地反映我在寻找什么。我假设在 before_transition 上停止状态转换会停止回滚,但事实并非如此。
    • 你做不到。一旦代码被状态机启动(和处理),它就已经被包装在一个事务中。
    • 这个答案比 IMO 文档要好。非常翔实,谢谢。特别是关于从 before_transition 返回 false 的部分。当我第一次使用 state_machine 时,这让我大吃一惊。
    【解决方案2】:

    您可以尝试通过执行以下操作取消转换到下一个状态:

    before_transition :apple => :orange do
      if core.things.blank?
        errors.add(:core, 'must have one thing')
        throw :halt
      end
    end
    

    这样,如果 core.things 为空,则 core 会出现错误,并且过渡会被取消。我认为它也不会对数据库进行任何更改。虽然没有尝试过这段代码,但只是阅读了它的源代码。鉴于上面的代码,可能会导致更多的代码来捕获异常,那么下面的方法怎么样?

    def orange_with_validation
      if core.things.blank? && apple?
        errors.add(:core, 'must have one thing')
      else
        #transition to orange state
        orange
      end
    end
    

    您可以在需要验证的地方使用上面的代码,然后再将其转换为橙色状态。这种方法允许您解决 state_machine 回调的限制。在为向导表单提供动力的控制器中使用它会阻止表单进入下一步,并且会在验证失败时避免任何数据库命中。

    【讨论】:

    • 这是一个有趣的想法,但是我必须编写代码来捕获这个异常,所以它最终并没有真​​正减少围绕持久性的代码
    • 我上面提供的代码可能只是解决您问题的方法。它允许您在对象保存之前触发验证。您只需要在需要该行为的地方使用#orange_with_validation,而不仅仅是#orange
    【解决方案3】:

    我还是新手,但不是

     validates
    

    而不是

    validate
    

    http://edgeguides.rubyonrails.org/active_record_validations.html

    也只是阅读你必须在状态下进行验证的文档,我从未使用过 state_machine 但我认为是这样的:

    state :orange do
          validates_presence_of   :apple
    end
    

    【讨论】:

    【解决方案4】:

    Rails 正在为状态寻找“验证”方法。但是 validate 是一种主动记录方法。您的所有模型都继承自活动记录,但 state 没有,因此它没有 validate 方法。解决这个问题的方法是定义一个类方法并在状态中调用它。所以假设你的模型叫做 Fruit,你可以有这样的东西

    class Fruit < ActiveRecord::Base
        def self.do_the_validation
            validate :validate_core
        end
    
        before_transition :apple => :orange, :do => :do_the_validation
    end
    

    我不确定你是否需要 self.此外,第二行可能需要:

    self.validate :validate_core
    

    我认为这应该可行。话虽如此,您是否有任何理由在过渡之前对其进行验证?为什么不自己放置验证?它应该始终验证。

    【讨论】:

    • RE:“始终验证”,在您达到特定状态之前不需要关联。
    • 没有工作。我在 validate 前面有无 self 都试过了。
    • 你能发布 validate_core 方法吗?
    • 完成。显然,我正在尝试将代码简化为我认为的基本部分,因为实际的状态机有更多的转换。
    【解决方案5】:

    验证方法是模型的类方法,因此您不能从传递给 state_machine 类方法的块中调用他,因为您有新的上下文。

    试试这个:

    YourModel < AR::B
      validate :validate_core
    
      state_machine :state, :initial => :some_state do
        before_transition :apple => :orange do |model, transition|
          model.valid?
        end
      end
    
      def validate_core
        if core.things.blank?
          errors.add(:core, 'must have one thing')
        end
      end
    end
    

    【讨论】:

      【解决方案6】:

      “首先停止 [ping] 状态机转换为 :orange”的目标听起来像是转换的守卫。 state_machine 通过转换定义上的 :if 和 :unless 选项支持这一点。与 ActiveModel 验证器一样,这些选项的值可以是 lambda 或表示要在对象上调用的方法名称的符号。

      event :orangify
        transition :apple => :orange, :if => lambda{|thing| thing.validate_core }
        # OR transition :apple => :orange, :if => :validate_core
      end
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-04-07
        • 1970-01-01
        • 1970-01-01
        • 2015-02-27
        • 1970-01-01
        • 1970-01-01
        • 2015-08-23
        • 1970-01-01
        相关资源
        最近更新 更多