迁移最佳实践

数据库迁移是敏感操作,处理不当可能导致数据丢失或服务中断。遵循最佳实践,确保迁移安全可靠。

版本化迁移

每次迁移对应一个版本号,按顺序执行:

var migrations = []func(*gorm.DB) error{
    V1CreateUsersTable,
    V2AddUserNickname,
    V3CreateOrdersTable,
    V4AddOrderStatus,
}

func RunMigrations(db *gorm.DB) error {
    for _, m := range migrations {
        if err := m(db); err != nil {
            return err
        }
    }
    return nil
}

func V1CreateUsersTable(db *gorm.DB) error {
    return db.Migrator().CreateTable(&User{})
}

func V2AddUserNickname(db *gorm.DB) error {
    return db.Migrator().AddColumn(&User{}, "nickname")
}

迁移记录表

记录已执行的迁移:

type Migration struct {
    ID        uint
    Name      string
    Batch     int
    ExecutedAt time.Time
}

func RunMigration(db *gorm.DB, name string, fn func(*gorm.DB) error) error {
    if db.Migrator().HasTable(&Migration{}) == false {
        db.Migrator().CreateTable(&Migration{})
    }

    var count int64
    db.Model(&Migration{}).Where("name = ?", name).Count(&count)
    if count > 0 {
        return nil
    }

    return db.Transaction(func(tx *gorm.DB) error {
        if err := fn(tx); err != nil {
            return err
        }
        return tx.Create(&Migration{Name: name}).Error
    })
}

备份数据

迁移前备份:

func BackupTable(db *gorm.DB, table string) error {
    backupTable := table + "_backup_" + time.Now().Format("20060102150405")
    return db.Exec(fmt.Sprintf("CREATE TABLE %s AS SELECT * FROM %s", backupTable, table)).Error
}

可回滚迁移

每个迁移提供 up 和 down:

type MigrationFunc struct {
    Up   func(*gorm.DB) error
    Down func(*gorm.DB) error
}

var migrations = map[string]MigrationFunc{
    "20240101000001_create_users": {
        Up: func(db *gorm.DB) error {
            return db.Migrator().CreateTable(&User{})
        },
        Down: func(db *gorm.DB) error {
            return db.Migrator().DropTable(&User{})
        },
    },
    "20240102000001_add_nickname": {
        Up: func(db *gorm.DB) error {
            return db.Migrator().AddColumn(&User{}, "nickname")
        },
        Down: func(db *gorm.DB) error {
            return db.Migrator().DropColumn(&User{}, "nickname")
        },
    },
}

迁移脚本分离

将迁移脚本放在独立目录:

migrations/
├── 20240101000001_create_users.sql
├── 20240102000001_add_nickname.sql
└── 20240103000001_create_orders.sql

使用迁移工具如 golang-migrate、goose 等。

开发与生产分离

开发环境用 AutoMigrate,生产环境用版本化迁移:

func RunMigrations(db *gorm.DB) error {
    if os.Getenv("ENV") == "production" {
        return runVersionedMigrations(db)
    }
    return db.AutoMigrate(&User{}, &Order{})
}

迁移检查清单

迁移前检查:

  • 已备份数据库
  • 迁移脚本在测试环境验证
  • 大表迁移评估时间
  • 迁移可回滚
  • 通知相关人员

迁移后检查:

  • 验证表结构正确
  • 验证数据完整
  • 验证应用功能正常
  • 记录迁移日志

零停机迁移

大表迁移避免锁表:

添加字段(MySQL)

ALTER TABLE users ADD COLUMN nickname VARCHAR(50), ALGORITHM=INPLACE, LOCK=NONE;

创建索引

CREATE INDEX idx_name ON users(name), ALGORITHM=INPLACE, LOCK=NONE;

分批更新数据

func BatchUpdate(db *gorm.DB) error {
    batchSize := 1000
    for {
        result := db.Model(&User{}).
            Where("processed = ?", false).
            Limit(batchSize).
            Update("processed", true)
        
        if result.RowsAffected == 0 {
            break
        }
        time.Sleep(100 * time.Millisecond)
    }
    return nil
}

迁移工具推荐

golang-migrate

migrate -path ./migrations -database "mysql://user:pass@tcp(localhost:3306)/db" up

goose

goose mysql "user:pass@tcp(localhost:3306)/db" up

GORM 自带

适合简单场景,复杂迁移建议用专业工具。

实际案例

完整的迁移系统

package migration

import (
    "gorm.io/gorm"
)

type Migration struct {
    Version   string
    Name      string
    Up        func(*gorm.DB) error
    Down      func(*gorm.DB) error
}

type MigrationRecord struct {
    ID        uint
    Version   string
    ExecutedAt time.Time
}

func Migrate(db *gorm.DB, migrations []Migration) error {
    if err := db.AutoMigrate(&MigrationRecord{}); err != nil {
        return err
    }

    for _, m := range migrations {
        var record MigrationRecord
        result := db.Where("version = ?", m.Version).First(&record)
        if result.Error == nil {
            continue
        }

        if err := m.Up(db); err != nil {
            return err
        }

        db.Create(&MigrationRecord{
            Version:    m.Version,
            ExecutedAt: time.Now(),
        })
    }

    return nil
}

使用示例

func main() {
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

    migrations := []migration.Migration{
        {
            Version: "20240101000001",
            Name:    "create_users",
            Up: func(db *gorm.DB) error {
                return db.Migrator().CreateTable(&User{})
            },
            Down: func(db *gorm.DB) error {
                return db.Migrator().DropTable(&User{})
            },
        },
    }

    migration.Migrate(db, migrations)
}

小结

数据库迁移要谨慎。版本化管理、可回滚、零停机是核心原则。生产环境使用专业迁移工具,开发环境可以用 AutoMigrate 提高效率。