state_machines-activerecord icon indicating copy to clipboard operation
state_machines-activerecord copied to clipboard

Transient attributes quirks

Open boardfish opened this issue 4 years ago • 1 comments

I have a state machine on a class that inherits from ActiveRecord::Base that's something like this:

    state_machine :state do
      state :one, :two do
        validate { |record| record.validate_has_gubbins }
      end
      state :three

      event :one_to_two do
        transition one: :two # , if: ->(m) { m.gubbins.present? }
      end

      event :two_to_three do
        transition two: :three
      end
    end

It's based on an attribute that's not persisted to the database - a transient attribute. I define this with an attr_accessor and set its initial value in an after_initialize hook. The initial value is figured out from the current state of the record.

This is a useful approach because the state machine can be changed without a significant negative impact on existing records - if a new state were introduced between one and two, for example, two would still be stored in the database and would leave all two records in that state, whereas if it's dynamically figured out, they'd just move back to one with all data intact. It's just as useful for validating records at different stages while being a little more flexible where you need it.

There are a couple of quirks with this approach at the moment, though:

  • The validator I've added up there doesn't run. I'm following the advice here to set the validation on both the one and two states. The fact that the validator doesn't run then means I need to use the commented part to protect that transition. I did some experimenting myself and the state block does get called, but the validate block does not.
  • initial: doesn't work. I tried setting it to a lambda and a symbol, and I also tried playing with initialize_state_machine(force: true). The fix as mentioned is to set its initial value in an after_initialize hook as you would any other transient attribute, but it'd be better to have it within the state machine declaration.

boardfish avatar Jan 22 '21 12:01 boardfish

I've since changed approaches - I'm now using a dynamic state machine as per the docs. Thus far, I think I'd recommend that, but I've got the following observations:

  • The state machine is on the Machine class rather than the object that gets passed in to Machine#new. You'll need to allow the machine to recognize the parent object if you want to use that in conditional transitions. Aim for something like if: ->(machine) { machine.parent. [...] }
  • You'll also need to set the initial state based on the parent object.

Tangentially related, but with transient attributes, use the ActiveRecord Attributes API.

boardfish avatar Jan 27 '21 12:01 boardfish