Go语言数据库迁移与版本管理
Go语言数据库迁移与版本管理
引言
数据库迁移是数据库开发中的重要环节,用于管理数据库 schema 的演变。Go语言中有多个优秀的数据库迁移工具,如 goose、golang-migrate 等。本文将深入探讨Go语言中的数据库迁移实践和版本管理策略。
一、迁移工具选择
1.1 golang-migrate
# 安装golang-migrate go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest # 创建迁移文件 migrate create -ext sql -dir db/migrations -seq create_users_table1.2 goose
# 安装goose go install github.com/pressly/goose/v3/cmd/goose@latest # 初始化迁移目录 goose init mysql # 创建迁移文件 goose create create_users_table sql二、迁移文件结构
2.1 迁移文件格式
-- +goose Up -- SQL in this section is executed when the migration is applied. CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- +goose Down -- SQL in this section is executed when the migration is rolled back. DROP TABLE users;2.2 版本控制
db/migrations/ ├── 000001_create_users_table.sql ├── 000002_create_posts_table.sql ├── 000003_add_email_index.sql └── 000004_add_status_column.sql三、程序化迁移
3.1 使用golang-migrate库
package main import ( "log" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/mysql" _ "github.com/golang-migrate/migrate/v4/source/file" ) func main() { // 创建迁移实例 m, err := migrate.New( "file://db/migrations", "mysql://user:password@tcp(localhost:3306)/testdb", ) if err != nil { log.Fatalf("Failed to create migrate instance: %v", err) } // 执行迁移 if err := m.Up(); err != nil && err != migrate.ErrNoChange { log.Fatalf("Migration failed: %v", err) } log.Println("Migration completed successfully") }3.2 程序化回滚
func RollbackMigration(steps int) error { m, err := migrate.New( "file://db/migrations", "mysql://user:password@tcp(localhost:3306)/testdb", ) if err != nil { return err } // 回滚指定步数 if err := m.Steps(-steps); err != nil { return err } return nil }四、迁移策略
4.1 增量迁移
func MigrateWithOptions() error { m, err := migrate.NewWithDatabaseInstance( "file://db/migrations", "mysql", databaseInstance, ) if err != nil { return err } // 设置迁移选项 m.Log = &customLogger{} // 执行迁移 if err := m.Up(); err != nil && err != migrate.ErrNoChange { return err } return nil } type customLogger struct{} func (l *customLogger) Printf(format string, v ...interface{}) { log.Printf(format, v...) }4.2 事务性迁移
-- +goose Up BEGIN; CREATE TABLE temp_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE ); INSERT INTO temp_users SELECT id, name, email FROM users; DROP TABLE users; ALTER TABLE temp_users RENAME TO users; COMMIT; -- +goose Down BEGIN; CREATE TABLE temp_users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE ); INSERT INTO temp_users SELECT id, name, email FROM users; DROP TABLE users; ALTER TABLE temp_users RENAME TO users; COMMIT;五、迁移管理最佳实践
5.1 迁移脚本规范
func ValidateMigrationScripts(dir string) error { files, err := os.ReadDir(dir) if err != nil { return err } for _, file := range files { if !strings.HasSuffix(file.Name(), ".sql") { continue } content, err := os.ReadFile(filepath.Join(dir, file.Name())) if err != nil { return err } contentStr := string(content) if !strings.Contains(contentStr, "-- +goose Up") { return fmt.Errorf("missing 'Up' directive in %s", file.Name()) } if !strings.Contains(contentStr, "-- +goose Down") { return fmt.Errorf("missing 'Down' directive in %s", file.Name()) } } return nil }5.2 迁移状态管理
type MigrationStatus struct { Version uint AppliedAt time.Time Dirty bool } func GetMigrationStatus(db *sql.DB) ([]MigrationStatus, error) { rows, err := db.Query(` SELECT version, applied_at, dirty FROM schema_migrations ORDER BY version DESC `) if err != nil { return nil, err } defer rows.Close() var statuses []MigrationStatus for rows.Next() { var status MigrationStatus err := rows.Scan(&status.Version, &status.AppliedAt, &status.Dirty) if err != nil { return nil, err } statuses = append(statuses, status) } return statuses, nil }六、集成到CI/CD
6.1 GitHub Actions
name: Database Migration on: push: branches: - main jobs: migrate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Install migrate run: go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest - name: Run migrations run: | migrate -path db/migrations -database "mysql://${{ secrets.DB_USER }}:${{ secrets.DB_PASSWORD }}@tcp(${{ secrets.DB_HOST }}:3306)/${{ secrets.DB_NAME }}" up6.2 迁移测试
func TestMigration(t *testing.T) { // 创建测试数据库 testDB, err := CreateTestDatabase() if err != nil { t.Fatalf("Failed to create test database: %v", err) } defer DropTestDatabase(testDB) // 执行迁移 m, err := migrate.NewWithDatabaseInstance( "file://db/migrations", "mysql", testDB, ) if err != nil { t.Fatalf("Failed to create migrate instance: %v", err) } if err := m.Up(); err != nil && err != migrate.ErrNoChange { t.Fatalf("Migration failed: %v", err) } // 验证迁移结果 rows, err := testDB.Query("SELECT COUNT(*) FROM users") if err != nil { t.Fatalf("Failed to query users table: %v", err) } defer rows.Close() var count int rows.Scan(&count) if count != 0 { t.Errorf("Expected 0 users, got %d", count) } }结语
数据库迁移是数据库开发中的关键环节,通过合理选择迁移工具、编写规范的迁移脚本和集成到CI/CD流程,可以确保数据库 schema 的安全演变。希望本文的实践经验能帮助你更好地管理Go语言项目中的数据库迁移。
