Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
86: Rails Migrations In Depth
I see this happen constantly with developers moving from a "prototype" mindset to a "production" mindset: they treat migration files like they treat Ruby classes. They think, "I made a mistake in the column name in that migration from yesterday; I'll just open the file, fix the typo, and run rails db:migrate again."
Here is why that is a dangerous mistake. Let's say you ran a migration that created a posts table with a column called auther_name (a typo). You ran bin/rails db:migrate, and the table was created in your local database. You then notice the typo, change the code in the migration file to author_name, and run the migrate command again. Rails will tell you Nothing to do.
It doesn't care that you changed the text in the file. Rails tracks which migrations have already run by storing their timestamps in a special internal table called schema_migrations. Once a timestamp is in that table, Rails ignores that file forever. You've now created a "drift" where your code says one thing, but your actual database says another. If a teammate pulls your code, they'll get the "fixed" version; but your production server is still stuck with auther_name. You've just broken your deployment pipeline.
Migrations are a Ledger, Not a Blueprint
The most important shift in your mental model needs to be this: Migration files are a journal of changes, not a definition of the current state.
If you need to change a column name, you don't edit the migration that created the column. You create a new migration that alters the existing column. This preserves the history of the database. When a new developer joins the project, they run all migrations in order, recreating the exact evolution of the database schema that you experienced.
# Don't edit the old file. Create a new one:
# bin/rails generate migration RenameAutherNameToAuthorInPosts
class RenameAutherNameToAuthorInPosts < ActiveRecord::Migration[7.0]
def change
rename_column :posts, :auther_name, :author_name
end
end
This is why we have db/schema.rb. That file is the blueprint. It is the authoritative source of what your database looks like right now. You should almost never edit schema.rb manually; it is automatically updated by Rails every time you run a migration.
When the change Method Isn't Enough
For most things—adding columns, creating tables—the change method is magic. Rails looks at the command and knows exactly how to reverse it. If you add_column in change, Rails knows that rollback means remove_column.
But Rails isn't omniscient. Some operations are "irreversible." Take remove_column. If you tell Rails to remove :email from the users table, and then you decide to rollback that migration, Rails has no idea what the data type of that column was or what options (like null: false) were set. It can't just "guess" how to put it back.
In these cases, you have to ditch change and use the explicit up and down methods. I personally prefer this for any complex data migration because it forces me to think about the "undo" path before I push to production.
class RemoveEmailFromUsers < ActiveRecord::Migration[7.0]
def up
remove_column :users, :email
end
def down
# We explicitly define how to rebuild the column if we rollback
add_column :users, :email, :string, null: false
end
end
If you use change for a destructive action without providing the necessary information for a rollback, you'll hit a nasty ActiveRecord::IrreversibleMigration error when you try to go back. In a production emergency, that's the last thing you want to see.
📋 Practical Task
Exercise: Implementing a Reversible Unique Constraint on User Profiles
You are working on a project where the user_profiles table needs a unique constraint on the username column to prevent duplicates. However, the team is worried about the migration failing if there are already duplicate usernames in the development database, and they want to ensure the migration is fully reversible.
Your Goal: Create a migration that adds a unique index to the username column of the user_profiles table.
- Constraint: Do not use the
changemethod. Useupanddownto ensure the index is explicitly created and removed. - Requirement: The index should be named
unique_username_index. - Validation: After writing the migration, describe (in a comment or a separate text file) what would happen if you tried to run this migration on a database that already contains two users with the username "rails_pro".
Starter Schema:
create_table "user_profiles" do |t|
t.string "username"
t.text "bio"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
There are no comments for now.