iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Rust

Rust 後端入門系列 第 16

Day 16 Axum 撰寫 migration

  • 分享至 

  • xImage
  •  

為什麼要用 migration?遷移的目的與設計原則

  • 目的:
    • 把資料庫 schema 的變更以可追溯、可版本化的方式管理(像程式碼一樣)。
    • 保證團隊成員、CI 與部署環境能以一致的流程應用相同 schema 變更。
    • 支援資料庫結構的回溯(rollback)與重建(在測試或 staging)。
  • 設計原則:
    • 每個 migration 檔案應該是小而單一的變更(單一責任),便於審查與回滾。
    • migration 要可重複執行(至少在不同環境可正確 apply),且能被 CI 自動執行。
    • 對於不可逆的變更(例如資料刪除),紀錄明確並盡量避免硬編碼破壞性操作。

sqlx-cli 簡介與安裝

  • sqlx-cli 是 sqlx 生態提供的 CLI 工具,可用來:

    • 建立與套用 migration(migrate add / migrate run / migrate revert 等)
    • 產生 offline query cache(cargo sqlx prepare)
    • 其他資料庫管理輔助
  • 安裝(建議用 rustup / cargo):

    • 推薦安裝範例(包含 Postgres 與 rustls,避免 openssl 問題):

      cargo install sqlx-cli --no-default-features --features postgres,rustls
      
    • 若要指定版本,加上 --version 參數,例如:

      cargo install sqlx-cli --no-default-features --features postgres,rustls --version 0.7.0
      
    • 安裝完成後,確認:

      sqlx --version
      
  • 注意:

    • sqlx-cli 會在執行 migration 時連到 DATABASE_URL 指定的資料庫(需可連線)。
    • 若使用 Linux / macOS,可能需要把 $HOME/.cargo/bin 加到 PATH。Windows 則看 cargo 安裝位置。

用 sqlx-cli 建立 migration(語法、檔名慣例、up/down)

  • 初始化 migration 資料夾(若尚未建立):

    sqlx migrate add init
    

    這個指令會在專案下建立一個 migrations/ 目錄(或更新)並產生遷移檔案。

  • migration 檔案命名慣例:

    • sqlx-cli 會用時間戳或遞增 ID 做前綴,例如:
      migrations/20250929-0001-init.sql
    • 檔案內部通常包含 UP SQL,若要支援 DOWN(回滾),可建立另一個 .down.sql,或使用單一檔案內顯式分隔(sqlx-cli 的格式為單一 SQL 檔案,使用特殊分隔?目前 sqlx-cli 的標準做法是:每個 migration 目錄內放 up.sql 與 down.sql 或單一 .sql 但常見慣例為 up.sql / down.sql)。實作上以版本為準:以 sqlx-cli v0.7 為例,建立 migration 會產生 single .sql 文件,內含 up 和 down 區塊以 SQL comment 分隔並不是標準;較穩妥的作法是用 sqlx-cli 的 migrate add 會產生一個目錄,內含 up.sql 與 down.sql。
  • 新增 migration 範例:

    sqlx migrate add create_users_table
    

    執行後會在 migrations/ 目錄下建立一個新檔案,例如:
    20250930040016_create_users_table.sql

範例:建立 users 表的 migration(SQL 範例)

  • 建立 users 表:
-- migrations/20250930040016_create_users_table.sql

CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
  • 重要說明:
    • 使用 IF NOT EXISTS / IF EXISTS 可以提升 idempotency(在某些情況避免錯誤)。
    • 資料刪除為不可逆操作,務必在變更前備份或在 migration 中寫明注意事項。

在開發流程中執行 migration(本地、CI / 部署)

  • 本地開發執行:

    sqlx migrate run
    

    這會依序執行 migrations 資料夾下尚未套用的 migration。

  • CI / 部署:

    • 在 CI pipeline 或部署前,先執行 sqlx migrate run(或在 release 步驟中執行),以保證資料庫 schema 已更新。
    • 建議在 CI 中使用可用的測試資料庫(容器化或 ephemeral DB),以便安全執行 migration。
    • 在生產環境,先在 staging 上跑一次,確認 migration 時效與相容性,特別是會花較久時間的 DDL(例如新增大量資料的 column、建立大型 index、reindex 等)。
  • 逐步發布(zero-downtime)策略:

    • 避免一次變更同時修改應用程式與 DB schema 的行為(可能造成不相容)。
    • 採取先新增 column(nullable)、在程式端同時支援舊/新欄位、再回頭填資料與設定 NOT NULL,最後移除舊欄位的方式,以支援平滑部署。

在 Axum 專案啟動時執行 migration

  • 有兩種常見做法:

    A) 用 sqlx-cli 在部署腳本或 CI 中執行 migrate run(外部管理)

    B) 在應用程式啟動時,程式內呼叫 sqlx::migrate! 或 sqlx::Migrate 來自動套用(程式啟動時自動 migration)

  • 優缺點:

    • A 的好處是部署時控制明確;B 的好處是部署比較簡單,但需注意多個實例同時啟動競爭套用 migration 的情況(需要鎖或確保 migration DB table 的原子性)。
  • 程式內自動套用的範例(Axum + sqlx):

    • 假設已在專案根目錄建立 migrations/ 目錄(上面已用 sqlx migrate add 建立)

    • 在 Cargo.toml 啟用 sqlx 的 migrate feature:
      sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "migrate"] }

    • main.rs 範例(連接資料庫部分,請參考前一天的程式):

      use sqlx::migrate::MigrateDatabase;
      async fn main() {
      
      	//...
      
      	match sqlx::postgres::Postgres::database_exists(&database_url).await {
      	        Ok(exists) => {
      	            if !exists {
      					println!("資料庫不存在,嘗試建立...");
      	                if let Err(e) = sqlx::postgres::Postgres::create_database(&database_url).await {
      	                    eprintln!("建立失敗: {}", e);
      	                }
      	            }
      	        }
      	        Err(e) => eprintln!("檢查資料庫是否存在失敗: {}", e),
      	    }
      	if let Err(e) = sqlx::migrate!("./migrations").run(&pool).await {
      	      eprintln!("migrations失敗: {}", e);
      	      std::process::exit(1);
      	  }
      	println!("成功完成 migrations");
      }
      

權限、版本控制與團隊協作建議

  • 權限:
    • 執行 migration 的 DB user 需要有建立/修改 schema 的權限(CREATE、ALTER、DROP、CREATE INDEX 等)。
    • 生產環境可採用更嚴格的權限策略:由 CI/CD pipeline 的 DB user 執行 migration,而應用程式則用較低權限的 user。但要確保 pipeline 有適當的密鑰管理。
  • 版本控制:
    • 將 migrations/ 資料夾納入 Git(或其他 SCM),每次變更都透過 PR 審查,便利回溯與審核。
  • 團隊協作:
    • 每個 migration 檔案應附上描述(在 PR 中)與執行評估(是否會鎖表、是否會長時間執行)。
    • 在變更 schema 前,與 DBA 或其他同仁討論可能的效能影響(index、查詢計劃改變)。

常見問題與除錯

  • sqlx migrate run 顯示連線錯誤:
    • 檢查 DATABASE_URL 是否正確、DB 是否啟動、port 與 host 設定是否為預期(127.0.0.1 vs localhost)。
  • migration 執行中斷且部分套用:
    • 檢查資料庫內管理遷移狀態的 table(sqlx_migrations 等),找出最後成功套用到哪個 migration。必要時手動修正或 revert 再重新 apply。
  • CREATE INDEX CONCURRENTLY 無法在 transaction 中:
    • 如果 migration runner 把 up.sql 放在 transaction 中執行,會導致這類語句失敗。處理方式:
      • 把該 migration 分離成單獨步驟,並在 runner 中特別處理(不以 transaction 執行該 migration)。
      • 或自行在 up.sql 中先建立非 concurrent index,或採用其他策略。
  • 多實例競爭套用 migration:
    • 在部署時只讓一個實例負責 migration(例如在部署腳本加一個步驟),或在程式內加入 leader election 機制來避免多台同時執行。

上一篇
Day15 Rust 專案連接資料庫(PostgreSQL)
系列文
Rust 後端入門16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言