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:
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
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.
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.
Step 1: PR #1
In this initial PR, we will implement the following changes:
-
Create a migration: AddStateToPosts.
class AddStateToPosts < ActiveRecord::Migration[7.0] def change add_column :posts, :state, :string end end
-
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.
class Post < ApplicationRecord before_validation :sync_state // AASM code private def sync_state self.state = aasm_state end end
-
Develop a rake task (single_run.rake) to migrate existing aasm_state values to the new state column.
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
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"
Step 2: PR #2
In this subsequent PR, we can proceed with the following changes:
-
Remove the AASM-related code from the Post model.
-
Add the enum state to the Post model, mapping the AASM states.
enum state: { draft: "draft", published: "published", archived: "archived", }, _default: "draft"
-
Rewrite the actions triggered during state transitions to utilize the enum-based approach.
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
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:
def archive
resource.archive!
end
which will now become:
def archive
resource.archive
end
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 🎉
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.