Ruby Workflow Issue During Migration

2019-07-23 07:13发布

问题:

I am using Ruby Workflow in my ActiveRecords using Gem: Workflow

Existing Running Code contains:

  • I am having an ActiveRecord: X
  • I am having two Migrations already:
    • (Ref1) CreateX migration (which creates table X)
    • (Ref2) CreateInitialEntryInX migration (which creates one entry in table X)

New Changes:

  • Now I wanted to add workflow in ActiveRecord X, hence I did:
    • (Ref3) I added the workflow code in ActiveRecord Model X (mentioning :status as my workflow field)
    • (Ref4) AddStatusFieldToX migration (which adds :status field in table X)

Now when I run rake db:migrate after the changes, the (Ref2) breaks cos Migration looks for :status field as it is mentioned in ActiveRecord Model in the Workflow section, but :status field has not been added yet as migration (Ref4) has not executed yet.

Hence, all the builds fail when all migrations are run in sequence, Any solution to this? I do not want to resequence any of the migration or edit any old existing migrations.

My Model looks like:

  class BaseModel < ActiveRecord::Base
      #
      # Workflow to define states of Role
      #
      # Initial State => Active
      #
      # # State Diagram::
      #   Active  --soft_delete--> Deleted
      #   Deleted
      #
      # * activate, soft_delete are the event which triggers the state transition
      #
      include Workflow
      workflow_column :status
      workflow do
        state :active, X_MODEL_STATES::ACTIVE do
          event :soft_delete, transitions_to: :deleted
        end
        state :deleted, X_MODEL_STATES::DELETED

        on_transition do |from, to, event, *event_args|
          self.update_attribute(:status, to)
        end
      end

      def trigger_event(event)
        begin
          case event.to_i
            when X_MODEL_EVENTS::ACTIVATE
              self.activate!
            when X_MODEL_EVENTS::SOFT_DELETE
              self.soft_delete!
          end
        rescue ....
      end
  end

  class X_MODEL_STATES
    ACTIVE      = 1
    DELETED     = 2
  end

  class X_MODEL_EVENTS
    ACTIVATE      = 1
    SOFT_DELETE   = 2
  end

# Migrations(posting Up functions only - in correct sequence)
#--------------------------------------------------

#1st: Migration - This is already existing migration
CreateX < ActiveRecord::Migration
  def up
    create_table :xs do |t|
      t.string :name
      t.timestamps null: false
    end
  end
end

#2nd: Migration - This is already existing migration
CreateInitialX < ActiveRecord::Migration
  def up
    X.create({:name => 'Kartik'})
  end
end

#3rd: Migration - This is a new migration
AddStatusToX < ActiveRecord::Migration
  def up
    add_column :xs, :status, :integer
    x.all.each do |x_instance|
      x.status = X_MODEL_STATES::ACTIVE
      x.save!
    end
  end
end

So, when Migration#2 runs, it tries to find :status field to write is with initial value of X_MODEL_STATES::ACTIVE as it is mentioned in Active Record Model files workflow as: workflow_column :status and does not find the field as Migration#3 is yet to execute.

回答1:

You can wrap up your workflow code by a check for column_name.

if self.attribute_names.include?('status')
  include Workflow
  workflow_column :status
  workflow do
    ...
  end
end

This will result in running workflow code only after 'AddStatusToTable' migration ran successfully.



回答2:

This is a pain, as your models need to be consistent for migrations. I don't know any auto solutions for this.

Theoreticaly the best way would be to have model code versions binded with migrations. But I don't know any system that allows this.

Every time I do big models refactoring i experience this problem. Possible solutions to this situation.

  1. Run migrations on production manually to assure consitent state between migrations and models

  2. Temporarily comment out workflow code in model to run blocking migration, then deploy, then uncomment workflow code and move on with deploy and next migrations

  3. Version migrations and model changes on branches, so they are consistent. Deploy to production and run migration by chunks

  4. Include temp workarounds in the model code, and remove them from source after migrations on production are deployed.

  5. Monkey-patch model in migration code for backwards compatibility. In your situation this would be to dynamically remove 'workflow' from model code, which might be hard, and then run migration

All solutions are some kind of dirty hacks, but it's not easy to have migrations and model code versioned. The best way would be to deploy in chunks or if need to deploy all at once use some temp code in model and remove it after deploy on production.



回答3:

THANKS ALL I found the solution to this, and i am posting it here. The problems to issue here were:

  • Add :status field to ActiveRecord Model X without commenting out Workflow Code and not let Workflow disallow creation of an instance in Table X during migration.
  • Secondly, add an if condition to it as specified by @007sumit.
  • Thirdly, to be able to reload Model in migration with the updated column_information of Model X

Writing whole code solution here:

  class BaseModel < ActiveRecord::Base
      #
      # Workflow to define states of Role
      #
      # Initial State => Active
      #
      # # State Diagram::
      #   Active  --soft_delete--> Deleted
      #   Deleted
      #
      # * activate, soft_delete are the event which triggers the state transition
      #

      # if condition to add workflow only if :status field is added
      if self.attribute_names.include?('status')
        include Workflow
        workflow_column :status
        workflow do
          state :active, X_MODEL_STATES::ACTIVE do
            event :soft_delete, transitions_to: :deleted
          end
          state :deleted, X_MODEL_STATES::DELETED

        end
      end

      def trigger_event(event)
        ...
      end
  end

  class X_MODEL_STATES
    ...
  end

  class X_MODEL_EVENTS
    ...
  end

# Migrations(posting Up functions only - in correct sequence)
#--------------------------------------------------

#1st: Migration - This is already existing migration
CreateX < ActiveRecord::Migration
  def up
    create_table :xs do |t|
      t.string :name
      t.timestamps null: false
    end
  end
end

#2nd: Migration - This is already existing migration
CreateInitialX < ActiveRecord::Migration
  def up
    X.create({:name => 'Kartik'})
  end
end

#3rd: Migration - This is a new migration to add status field and 
# modify status value in existing entries in X Model
AddStatusToX < ActiveRecord::Migration
  def up
    add_column :xs, :status, :integer

    # This resets Model Class before executing this migration and 
    # Workflow is identified from the if condition specified which was 
    # being skipped previously without this line as Model Class is  
    # loaded only once before all migrations run.
    # Thanks to post: http://stackoverflow.com/questions/200813/how-do-i-force-activerecord-to-reload-a-class

    x.reset_column_information
    x.all.each do |x_instance|
      x.status = X_MODEL_STATES::ACTIVE
      x.save!
    end
  end
end

@stan-brajewski Now, this code can go in one deployment. Thanks all for inputs :)