How to migrate AASM to ENUM while keeping users in mind
Tags: beginners, webdev, rails
Published: 2023-05-31 09:41:09

## Introduction

In this article, we will address a common challenge when migrating from the AASM state machine to the Rails built-in enum. The main problem we aim to solve is ensuring a seamless transition without causing disruptions or data inconsistencies for users. To tackle this issue, we will follow a systematic approach and split the migration process into two pull requests. By carefully implementing these changes, we can ensure a smooth transition while maintaining data integrity. Let's explore the solution in detail.


**Why Migrate to Enum?**
As part of our teams commitment to align with Rails conventions, we have decided to migrate models that previously utilized AASM to enum. Migrating from AASM to the Rails enum feature brings benefits such as simplified code maintenance and improved performance through optimized database queries. By leveraging the native enum functionality, developers can enhance code readability, adhere to Rails conventions, and tap into the extensive community support available for working with enums in Rails.


This is how a aasm state machine might look like for a `post` model:

```ruby
include AASM

aasm column: :aasm_state, whiny_transitions: false do
  state :draft, default: true
  state :published, enter: [:actions_on_publish]
  state :archived, enter: [:actions_on_archive]

  event :publish do
    transitions from: :draft, to: :published
  end

  event :archive do
    transitions from: :published, to: :archived
  end

  event :unarchive do
    transitions from: :archived, to: :published
  end
end

def actions_on_publish
  ### actions to perform when publishing a post
end

def actions_on_archive
  ### actions to perform when archiving a post
end
```
&nbsp;

**The Challenge**
The main challenge arises when we deploy the code without considering the existing data and user experience. If we were to directly switch to the new enum state column, users would encounter unexpected behavior. For instance, until the migration is completed, users would not see their old posts on the post index page. This occurs because the index page would rely on the new state column, which initially lacks the migrated data. The same issue applies to recruiters accessing the post data.

&nbsp;
**Proposed Solution**
To tackle this problem, we can divide the migration process into two pull requests (PRs) and leverage a rake task to facilitate the data migration. By following this approach, we can ensure a seamless transition without causing inconvenience to users.

&nbsp;
**Step 1: PR #1**
In this initial PR, we will implement the following changes:

1. Create a migration: AddStateToPosts.

    ```ruby
    class AddStateToPosts < ActiveRecord::Migration[7.0]
      def change
          add_column :posts, :state, :string
      end
    end
    ```
    &nbsp;

2. Implement a sync_state method in the Post model to keep the enum state synchronized with the existing aasm_state until we remove the aasm_state in PR#2. Also add the method as a before_validation.

    ```ruby
    class Post < ApplicationRecord

      before_validation :sync_state

      // AASM code

      private

      def sync_state
          self.state = aasm_state
      end
    end
    ```
    &nbsp;

3. Develop a rake task (single_run.rake) to migrate existing aasm_state values to the new state column.

    ```ruby
    desc "2023-05-28: Migrate post aasm state to enum state"
    task migrate_post_aasm_state_to_enum_state: :environment do
      Post.all.distinct.pluck(:aasm_state).each do |state|
          posts = Post.where(aasm_state: state, state: nil)
          puts "Starting to migrate #{posts.count} #{state} posts"
          posts.in_batches do |batch|
          batch.update_all(state: state)
          end
      end
      puts "🏁 All done! 🏁"
    end
    ```
&nbsp;

At this point you can also add a test to validate that the enum state is synced properly every time a Post's aasm_state is updated.

Once the PR is merged run following code:

`rake "single_run:migrate_post_aasm_state_to_enum_state"`
&nbsp;

**Step 2: PR #2**
In this subsequent PR, we can proceed with the following changes:

1. Remove the AASM-related code from the Post model.

2. Add the enum state to the Post model, mapping the AASM states.

    ```ruby
    enum state: {
      draft: "draft",
      published: "published",
      archived: "archived",
    }, _default: "draft"
    ```
    &nbsp;

3. Rewrite the actions triggered during state transitions to utilize the enum-based approach.

    ```ruby
    def publish
      return if published?
      published!
      ### perform additional actions when publishing a post
    end

    def archive
      archived!
      ### perform additional actions when archiving a post
    end

    def unarchive
      published! if archived?
      ### perform additional actions when unarchiving a post
    end
    ```
    &nbsp;

Last but not least, check in your code where the aasm_state was being called on an instance and swap it out with the new enum state.

For example:
`posts.where.not(aasm_state: "draft")`

will now become:
`posts.where.not(state: "draft")`

Also look out where the old actions where being called with a bang (!) if you want the new instance methods to work anymore.

In a controller for the posts you may had something like:

```ruby
  def archive
    resource.archive!
  end
```

which will now become:

```ruby
  def archive
    resource.archive
  end
```
&nbsp;

Of course you could also do `resource.archived!` but that would only change the state and not call the instance method `archive`.

And that's it, we are done :tada:

**Conclusion:**
By following this two-step approach and employing a rake task for data migration, we can smoothly transition from the AASM state machine to Rails' built-in enum without causing problems or downtime for users. 

It is crucial to consider existing data and maintain a consistent user experience during such migrations. By adhering to Rails conventions and implementing the proposed solution, we ensure code consistency and improve the maintainability of our application. 

Thank you for reading the whole article, it means a lot to me. This is the first of hopefully more to come.