在許多框架像是 Django、Rails 裡面都有 migration 的設計,而 migration 是一種用來「管理資料庫結構(Schema Evolution)」的版本化腳本,讓我們可以用指定程式碼去新增、修改、刪除資料庫裡的表格與欄位。
Migration 是版本控制的 DB schema。
在 Rails,會透過 timestamp 命名來確保 migration 的先後順序。
db/migrate/20260410123456_create_users.rb
隨著協作開發的推進,專案的 migration 也會逐漸增加,那我們要怎麼知道到底 database (資料庫)已經應用了哪些 migration 呢?
在 Rails 首次進行 migrate 時,會同時在資料庫建立 schema_migrations 表格,裡面紀錄了專案目前已經應用的 migration 檔案。
而當我們使用:
rails db:migrate:status
Rails 會去比對 db/migrate 裡的 migrations 檔案和 schema_migrations 表格,並輸出這樣的資料:
Status Migration ID Migration Name
--------------------------------------------------
up 20240401010101 Create users
down 20240402020202 Add email to users
其中 up 代表「已應用的 migrations」,down 代表「未執行的 migrations」。
rails db:migrate:status本質上是比對 migration 檔案 vs schema_migrations table。
Migration 長怎樣?
當我們想建立一個新的 migration 時:
rails generate migration AddEmailToUsers email:string
這段指令代表的意義為:
- 在 users table 新增 email 欄位
- email 欄位的型別為 string
而這個新的 migration 檔案的內容會是:
# 20260411010530_add_email_to_users.rb
class AddEmailToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :email, :string
end
end
這裡面有幾個比較細節的部分:
ActiveRecord::Migration代表這是 Action Record 提供的功能。[8.0]代表 migration 是依照 Rails 8.0 的 migration API 規格建立。def change代表對資料庫產生變化的內容。
當我們使用 rails db:migrate 時,Rails 會比對 db/migrate 裡的檔案,以及資料庫中的 schema_migrations 表格,並執行應用所有「尚未應用」的 migrations。
在應用的過程中,會跳過已經應用的 migrations,並且把 migrations 的版本號(例如:20260411010530)寫入 schema_migrations,同時更新 db/schema.rb 的結構。
Rails 執行 migration 裡的 change 函數後,會對資料庫(以 PostgreSQL 為例),執行類似這樣的 SQL:
ALTER TABLE users ADD COLUMN email character varying;
如何取消已經執行過的 migration?
如果我們想回滾(取消)已經應用的 migration,可以使用下面指令來回退最近應用一筆應用的 migration:
rails db:rollback
以前面的 20260411010530_add_email_to_users.rb 為例,當我們執行 rails db:rollback,實際上等同於執行 SQL 指令:
-- 將 users 的 email 欄位移除
ALTER TABLE users DROP COLUMN email;
這時候使用 rails db:migrate:status 查看,應該會看到:
up 20260410010101 Create users
up 20260410010202 Add name to users
down 20260410010303 Add email to users
最後一筆 migration 的狀態變成 down 的狀態。
如何取消多筆已經執行的 migration?
除了可以慢慢地一次又一次執行 rails db:rollback,也可以使用 rails db:rollback STEP=<步數> 進行多步回滾,例如:
# 回滾最近三筆狀態為 up 的 migration
rails db:rollback STEP=3
rails db:rollback STEP=1等同於rails db:rollback。
Rollback 回滾的意義
我們可以把 rollback (回滾)視為接近「復原(undo)」的一種反向操作,有點接近 ctrl + z,但也不是什麼狀況都可以執行 rollback。
以稍早的「在 users 表格新增 email 欄位」為例:
def change
add_column :users, :email, :string
end
執行 rollback 則會從 users 中移除 email 欄位。
但如果換成這個情境:
# 將 users.email 所有的資料都更新為 'test'
def change
execute "UPDATE users SET email = 'test'"
end
當執行 rollback 時,會期待復原所有的 users.email 為「原本的狀態」,但對於 Rails 根本無從得知什麼是「原本的狀態」,因此會跳出 ActiveRecord::IrreversibleMigration。
所以如果要讓這個 migration 可以執行 rollback,migration 的寫法應該要改成:
# 執行 migrate 會發生的行為
def up
execute "UPDATE users SET email = 'test'"
end
# 執行 rollback 會發生的行為
def down
execute "UPDATE users SET email = NULL"
end
透過 up 與 down 兩種方法,清楚讓 Rails 知道 migrate 與 rollback 對應的行為是什麼。
使用 VERSION 跳到指定的 migration
到目前聽下來,應該會感覺 rollback 很需要算數學,到底要回滾幾步?萬一不小心算錯步數怎麼辦?
Rails 也提供了 VERSION 指定版本的方式來改變應用的 migration,例如執行:
rails db:migrate VERSION=20240101010303
Rails 會自動計算當前的 migration 離 VERSION 差了哪些 migrations,然後執行對應步數的 migrate 或 rollback。
注意:使用 VERSION 一樣有機會碰到 IrreversibleMigration (無法逆應用)的錯誤。
rollback 與 VERSION 的使用情境
我們會在通常開發環境使用 rollback 調整資料庫的狀態,修正有問題的 migration,而 VERSION 則適合用來精確控制資料庫狀態,例如針對特定版本進行 debug 或是對齊團隊的資料庫狀態。
但在正式環境(production)裡,基本上不會使用 rollback,除了可能會遇到 irreversible 的錯誤,刪除欄位也會造成舊有資料消失,所以建議的做法是「寫新的 migration」進行修正。
