想像一個場景:開發者 A 在本地為 users
表新增了一個 age
欄位,並提交了對應的程式碼。
開發者 B 拉取了最新程式碼,但他的本地資料庫沒有 age
欄位,導致應用程式一啟動就崩潰。
這就是沒有資料庫遷移(Database Migration)時的混亂日常。
資料庫遷移,可以被理解為「資料庫結構的版本控制」。它將每一次對資料庫結構的變更(如新增表、新增欄位、修改索引)都記錄在一個個獨立的遷移檔案中。
這些檔案跟隨專案的程式碼一起被版本控制,從而確保任何拿到專案的人,都能將資料庫更新到與程式碼匹配的正確狀態。
我們將使用 golang-migrate/migrate
這個強大且與框架無關的工具來管理我們的遷移。
migrate
CLImigrate
是一個獨立的命令列工具,我們需要先安裝它。
# 在 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
.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 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
函式庫來實現。但這有風險。
手動執行:在生產環境中,強烈建議將資料庫遷移作為一個獨立的、在部署應用程式之前執行的步驟。原因如下:
CREATE
, DROP
)而帶來風險。這裡我們先選擇在程式中自動執行。
// 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 觀看