兩種寫法
Rails migration 可以寫成:
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :email, :string
end
end
或明確分開:
class AddEmailToUsers < ActiveRecord::Migration[7.1]
def up
add_column :users, :email, :string
end
def down
remove_column :users, :email
end
end
change 比較短,Rails 會自動推導出 rollback (db:rollback 時執行反向操作)。看起來 change 永遠好,但不是。
change 可以幫你推導的指令
官方清單 (API docs):
create_table↔drop_tableadd_column↔remove_columnadd_index↔remove_indexadd_reference↔remove_referenceadd_foreign_key↔remove_foreign_keychange_column_default(帶from:/to:)change_column_nullrename_column,rename_index,rename_table
這些 Rails 知道怎麼反過來。只用這些的 migration 可以放心用 change。
什麼時候一定要寫 up/down
1. change_column 改型態
def change
change_column :users, :age, :integer # ← 原本是什麼?
end
Rails 不知道「原本是什麼型態」,rollback 時不知道要改回啥。必須:
def up
change_column :users, :age, :integer
end
def down
change_column :users, :age, :string
end
2. 有 data migration (寫資料)
def change
add_column :users, :full_name, :string
User.reset_column_information
User.find_each { |u| u.update!(full_name: "#{u.first_name} #{u.last_name}") }
end
rollback 時怎麼還原?沒辦法。而且這整段 data migration 放 migration 裡本身就有問題 (下面再講),但如果真的要做,至少要明確 up / down:
def up
add_column :users, :full_name, :string
# data migration
end
def down
remove_column :users, :full_name
end
3. execute 直接跑 SQL
def change
execute "UPDATE users SET role = 'member' WHERE role IS NULL"
end
跟上面同理,Rails 不會去 parse SQL 然後反推。必須分成 up / down,或用 reversible:
def change
reversible do |dir|
dir.up { execute "UPDATE users SET role = 'member' WHERE role IS NULL" }
dir.down { execute "UPDATE users SET role = NULL WHERE role = 'member'" }
end
end
4. 真的不可逆
def up
remove_column :users, :legacy_token # 資料就這樣消失
end
def down
raise ActiveRecord::IrreversibleMigration
end
誠實寫出來,不要騙自己可以回。也可以在 change 裡用 Rails 提供的 block:
def change
remove_column :users, :legacy_token, :string # ← 注意:要指定原本的型態
end
remove_column 帶型態時 Rails 可以從 drop_column 推回 add_column,反向可逆。
我自己的 rule of thumb
- 純 schema change、且在「Rails 知道怎麼 reverse 的清單」裡 → 用
change。 - 含 data migration → 不應該跟 schema change 綁在同一個 migration,拆兩支。schema 那支用
change,data 那支用另一種機制 (下面講)。 - 含
executeSQL /change_column改型 → 寫up/down,或用reversible。 - 真的不可逆 → 寫
up,在down明確raise IrreversibleMigration。
不確定時優先寫 up/down。多打幾行總比半夜 rollback 時 migration 自己說「我不知道怎麼回去」好。
Data migration 不要放 schema migration 裡
即使你寫得漂漂亮亮的 reversible,實務上 schema migration 裡跑資料更新 還是有兩個麻煩:
1. 大 table 會 timeout
User.find_each { |u| u.update!(full_name: "#{u.first_name} #{u.last_name}") }
1000 萬筆 user,這段跑幾小時。migration 鎖表、deploy 掛在那、DBA 電話進來。
2. Model code 跟 schema 版本錯開
migration 寫在 20260424120000_backfill_full_name.rb 時用 User model,半年後 User 已經加了新欄位、新 callback、新 validation。這支 migration 在 rollback 或 schema re-setup 時又被跑一次,可能 behavior 完全不一樣。
比較好的 pattern:schema migration 只改 schema,data backfill 跑 rake task 或獨立的 maintenance job:
# lib/tasks/backfill_full_name.rake
namespace :backfill do
task full_name: :environment do
User.where(full_name: nil).find_each(batch_size: 1000) do |u|
u.update_columns(full_name: "#{u.first_name} #{u.last_name}")
end
end
end
Deploy flow:
- Deploy 帶 schema migration 的 commit →
add_column :users, :full_name。 rails backfill:full_name跑一次 (可中斷、可 monitor、可分批)。- Deploy 移除舊欄位 / 加 NOT NULL constraint 的 commit。
雖然步驟變多,但每一步都小、可回退、可 monitor。
小結
change給純 schema 用。change_column型態改變 /executeSQL / data migration → 寫up/down或用reversible。- 真的不可逆就誠實
raise IrreversibleMigration。 - Schema migration 跟 data migration 分開。schema 用
change,data 用 rake task。
REFERENCE / 2