Skip to content

Ruby on Rails:什麼是 Rails Migration

在許多框架像是 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

透過 updown 兩種方法,清楚讓 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」進行修正。