iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Software Development

Go Clean Architecture API 開發全攻略系列 第 12

資料庫整合 (二):資料庫遷移 (Migration)

  • 分享至 

  • xImage
  •  

想像一個場景:開發者 A 在本地為 users 表新增了一個 age 欄位,並提交了對應的程式碼。
開發者 B 拉取了最新程式碼,但他的本地資料庫沒有 age 欄位,導致應用程式一啟動就崩潰。
這就是沒有資料庫遷移(Database Migration)時的混亂日常。

資料庫遷移,可以被理解為「資料庫結構的版本控制」。它將每一次對資料庫結構的變更(如新增表、新增欄位、修改索引)都記錄在一個個獨立的遷移檔案中。
這些檔案跟隨專案的程式碼一起被版本控制,從而確保任何拿到專案的人,都能將資料庫更新到與程式碼匹配的正確狀態。

我們將使用 golang-migrate/migrate 這個強大且與框架無關的工具來管理我們的遷移。

第一步:安裝 migrate CLI

migrate 是一個獨立的命令列工具,我們需要先安裝它。

# 在 macOS 上使用 Homebrew
brew install golang-migrate

如果你使用其他作業系統,請參考官方安裝說明:https://github.com/golang-migrate/migrate

如果你不想要在本機上安裝,也可以使用 Docker 來執行 migrate

docker pull migrate/migrate:v4.19.0 # 拉取指定版本的 migrate 映像檔

第二步:建立我們的第一個遷移檔案

我們使用 migrate create 指令來建立遷移檔案。這些檔案將被存放在我們專案既有的 deployments/migrations 目錄中。

# -ext: 檔案副檔名
# -dir: 遷移檔案存放的目錄
# -seq: 使用連續的數字作為版本號
# create_users_table: 遷移的描述性名稱
migrate create -ext sql -dir deployments/migrations -seq create_users_table

# 或使用 Docker
docker run --rm -v $(pwd)/deployments/migrations:/migrations migrate/migrate:v4.19.0 create -ext sql -dir /migrations -seq create_users_table

執行後,你會在 deployments/migrations 目錄下看到兩個新檔案:

  • 000001_create_users_table.up.sql
  • 000001_create_users_table.down.sql

第三步:撰寫遷移 SQL

  • .up.sql:這個檔案定義了「向上」遷移,即應用此次變更的 SQL。

    -- deployments/migrations/000001_create_users_table.up.sql
    CREATE TABLE `users` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
      `email` varchar(64) NOT NULL,
      `password_hash` varchar(72) NOT NULL,
      `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      UNIQUE KEY `idx_users_email` (email)
    ) COMMENT='使用者帳號資料';
    
  • .down.sql:這個檔案定義了「向下」遷移,即撤銷此次變更的 SQL。這對於開發和修復錯誤時的回滾至關重要。

    -- deployments/migrations/000001_create_users_table.down.sql
    DROP TABLE IF EXISTS `users`;
    

第四步:migrate cli 指令

建立一個新的遷移檔案

migrate create -ext sql -dir deployments/migrations -seq $(name)

# 或使用 Docker
docker run --rm -v $(pwd)/deployments/migrations:/migrations migrate/migrate:v4.19.0 create -ext sql -dir /migrations -seq $(name)

執行所有待執行的遷移

migrate -database "${DB_URL}" -path deployments/migrations up

# 或使用 Docker
docker run --rm -v $(pwd)/deployments/migrations:/migrations migrate/migrate:v4.19.0 -database "${DB_URL}" -path /migrations up

Rollback 最後一次的遷移

migrate -database "${DB_URL}" -path deployments/migrations down 1

# 或使用 Docker
docker run --rm -v $(pwd)/deployments/migrations:/migrations migrate/migrate:v4.19.0 -database "${DB_URL}" -path /migrations down 1

移動到指定版本

migrate -database "${DB_URL}" -path deployments/migrations goto $(version)

# 或使用 Docker
docker run --rm -v $(pwd)/deployments/migrations:/migrations migrate/migrate:v4.19.0 -database "${DB_URL}" -path /migrations goto $(version)

第五步:執行策略 - 自動還是手動?

一個常見的問題是:「我應該在應用程式啟動時自動執行 migrate-up 嗎?」

  • 自動執行:在簡單的單體應用或開發環境中,為了方便,可以在 main.go 中透過程式碼呼叫 migrate 函式庫來實現。但這有風險。

    1. 權限問題:應用程式通常不應該擁有修改資料庫結構的權限,這可能導致安全風險。
    2. 競爭條件:在多實例部署中,可能會有多個實例同時嘗試執行遷移,導致錯誤。
    3. 不可預測性:自動遷移可能會在不適當的時間點發生,影響服務穩定性。
  • 手動執行:在生產環境中,強烈建議將資料庫遷移作為一個獨立的、在部署應用程式之前執行的步驟。原因如下:

    1. 安全性:避免應用程式因持有過高的資料庫權限(如 CREATE, DROP)而帶來風險。
    2. 原子性:將「部署程式」和「變更資料庫」這兩個重要操作分開,更容易定位問題。
    3. 擴展性:當你的應用程式有多個實例時,自動遷移可能導致多個實例同時嘗試修改資料庫,引發競爭條件。

這裡我們先選擇在程式中自動執行。

// internal/database/migrate.go

func (d *Database) MigrateUp() error {
	return d.migrateUpWithSourceURL("file://deployments/migrations")
}

func (d *Database) migrateUpWithSourceURL(sourceURL string) error {
	sql, err := d.db.DB()
	if err != nil {
		return err
	}

	driver, err := mysql.WithInstance(sql, &mysql.Config{})
	if err != nil {
		return fmt.Errorf("failed to create migrate driver: %w", err)
	}

	m, err := migrate.NewWithDatabaseInstance(sourceURL, "mysql", driver)
	if err != nil {
		return fmt.Errorf("failed to create migration instance: %w", err)
	}

	if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
		return fmt.Errorf("failed to run migration: %w", err)
	}

	log.Println("Migrations completed successfully")

	return nil
}

// cmd/api/main.go
err = app.Database.MigrateUp()
if err != nil {
    log.Fatalf("failed to run migrations: %v", err)
}

總結

透過 golang-migrate,我們為專案建立了一套健壯、可追溯、可回滾的資料庫結構管理流程。
這確保了團隊中每一位成員以及每一個環境(開發、測試、生產)的資料庫結構都能保持嚴格的一致性,是專業軟體工程中不可或缺的一環。

以上程式碼的完整內容可以到 Github 觀看


上一篇
資料庫整合 (一):使用 GORM 連接 MySQL
下一篇
Entity Model 與實作 Database Adapter
系列文
Go Clean Architecture API 開發全攻略19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言