Skip to content
Go
迁移与索引

迁移与索引

GORM 的 AutoMigrate 用于自动创建/更新表结构,适用于开发环境和简单的部署场景。本章覆盖迁移、索引、约束和版本管理。


AutoMigrate — 自动迁移

// 自动创建/更新表结构
db.AutoMigrate(&User{}, &Order{}, &Product{})

// 只迁移指定表
db.AutoMigrate(&User{})

AutoMigrate 会做什么:

操作 说明
✅ 创建不存在的表 根据模型结构生成 CREATE TABLE
✅ 添加新列 模型中新增的字段
✅ 添加索引和约束 index/uniqueIndex tag 指定的索引和约束
不会修改已有列的类型 varchar(100)varchar(200) 不会变
不会删除列 删除模型字段不会删除数据库列
不会重命名列 字段名变了会当作新增 + 旧列留着

🚨 陷阱:AutoMigrate 不会修改已有列的类型。如果你的模型从 string(对应 longtext)改为 string + size:255(对应 varchar(255)),表中列仍然是 longtext


表选项

// 创建表时指定存储引擎、字符集等
db.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4").AutoMigrate(&User{})

// 或者通过 TableName 方法返回注释

索引

普通索引

type User struct {
    gorm.Model
    Name  string `gorm:"index"`                    // 单列索引
    Email string `gorm:"index:idx_email"`           // 自定义索引名
    Age   int    `gorm:"index:idx_age_status,priority:1"` // 复合索引 + 优先级
    Status string `gorm:"index:idx_age_status,priority:2"`
}

生成:

CREATE INDEX idx_users_name ON users(name);
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_age_status ON users(age, status);

唯一索引

type User struct {
    gorm.Model
    Email string `gorm:"uniqueIndex"`                         // 唯一索引
    Phone string `gorm:"uniqueIndex:idx_phone"`               // 自定义索引名
    Name  string `gorm:"uniqueIndex:idx_name_deleted,priority:1,where:deleted_at IS NULL,expression:DEFAULT"` // 部分唯一索引
    DeletedAt gorm.DeletedAt `gorm:"uniqueIndex:idx_name_deleted,priority:2"`
}

生成:

CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_phone ON users(phone);
CREATE UNIQUE INDEX idx_name_deleted ON users(name, deleted_at) WHERE deleted_at IS NULL;

💡 最佳实践:软删除场景下,唯一索引应包含 WHERE deleted_at IS NULL 条件(部分唯一索引),避免"已删除的值"与新记录冲突。

索引选项

type User struct {
    gorm.Model
    Name string `gorm:"index:,length:100"`          // 前缀索引(仅 MySQL)
    Age  int    `gorm:"index:,sort:desc"`           // 降序索引(MySQL 8.0+)
    Type string `gorm:"index:,type:btree"`           // 索引类型
    Tags string `gorm:"index:,class:FULLTEXT"`       // FULLTEXT 全文索引(MySQL)
}

约束

外键约束

type User struct {
    gorm.Model
    Name      string
    CompanyID uint
    Company   Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
ALTER TABLE users
ADD CONSTRAINT fk_users_company
FOREIGN KEY (company_id) REFERENCES companies(id)
ON UPDATE CASCADE ON DELETE SET NULL;

🚨 陷阱gorm.Config.DisableForeignKeyConstraintWhenMigrating: true 会跳过外键创建。这是很多项目的默认配置,因为物理外键在某些场景下影响性能和数据迁移。

Check 约束

type Product struct {
    gorm.Model
    Name  string
    Price float64 `gorm:"check:price > 0"`
    Stock uint    `gorm:"check:stock >= 0 AND stock <= 99999"`
}

// 表级约束
type User struct {
    gorm.Model
    Name string
    Age  int
}
// 通过 migrator 添加表级约束
db.Migrator().CreateConstraint(&User{}, "age") // 或自定义约束名

自定义列类型

type User struct {
    gorm.Model
    ID       uint64 `gorm:"primaryKey;type:bigint unsigned AUTO_INCREMENT"` // MySQL 无符号自增
    Metadata string `gorm:"type:json"`        // JSON 列
    Position string `gorm:"type:geometry"`     // 空间列
    Tags     string `gorm:"type:jsonb"`        // PostgreSQL JSONB
    UUID     string `gorm:"type:uuid;default:gen_random_uuid()"` // PostgreSQL UUID
}

Migrator 接口 — 手动 DDL

当 AutoMigrate 不够用时,使用 Migrator 接口进行表结构操作:

// 检查表是否存在
db.Migrator().HasTable(&User{})
db.Migrator().HasTable("users")

// 创建表
db.Migrator().CreateTable(&User{})

// 删除表
db.Migrator().DropTable(&User{})
db.Migrator().DropTable("users")

// 重命名表
db.Migrator().RenameTable("users", "new_users")

// 列操作
db.Migrator().AddColumn(&User{}, "NewField")           // 添加列
db.Migrator().DropColumn(&User{}, "OldField")          // 删除列
db.Migrator().RenameColumn(&User{}, "old_name", "new_name") // 重命名列
db.Migrator().AlterColumn(&User{}, "FieldName")        // 修改列类型

// 检查列是否存在
db.Migrator().HasColumn(&User{}, "FieldName")

// 修改列类型
db.Migrator().AlterColumn(&User{}, "Age") // 使数据库列类型匹配当前模型定义

// 索引操作
db.Migrator().CreateIndex(&User{}, "Name")              // 创建索引
db.Migrator().CreateIndex(&User{}, "idx_name")          // 创建自定义名索引
db.Migrator().DropIndex(&User{}, "idx_name")            // 删除索引
db.Migrator().HasIndex(&User{}, "Name")                 // 是否已存在索引
db.Migrator().RenameIndex(&User{}, "old", "new")        // 重命名索引

// 约束操作
db.Migrator().CreateConstraint(&User{}, "Age")          // 创建约束
db.Migrator().DropConstraint(&User{}, "ConstraintName") // 删除约束
db.Migrator().HasConstraint(&User{}, "Name")            // 是否存在约束

// 查看当前数据库
db.Migrator().CurrentDatabase()

// 查看所有表
tables, _ := db.Migrator().GetTables()

// 查看列信息
columns, _ := db.Migrator().ColumnTypes(&User{})
for _, col := range columns {
    fmt.Printf("%s %s\n", col.Name(), col.DatabaseTypeName())
}

// 查看索引
indexes, _ := db.Migrator().GetIndexes(&User{})

迁移版本管理

AutoMigrate 不追踪版本,推荐以下方案:

方案一:Atlas(推荐)

Atlas 是 Go 生态中最流行的声明式迁移工具,支持 GORM:

# 安装
curl -sSf https://atlasgo.sh | sh

# 从 GORM 模型生成迁移文件
atlas migrate diff --env gorm

# 应用迁移
atlas migrate apply --env gorm

方案二:golang-migrate

go install -tags 'mysql,postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

# 创建迁移文件
migrate create -ext sql -dir migrations -seq create_users_table

# 执行迁移
migrate -path migrations -database "mysql://dsn" up

方案三:手写 SQL 迁移脚本

// migrate.go
type Migration struct {
    Version string
    Up      string
    Down    string
}

var migrations = []Migration{
    {"001_create_users", `
        CREATE TABLE users (
            id BIGINT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            created_at DATETIME(3),
            updated_at DATETIME(3),
            deleted_at DATETIME(3)
        );
    `, `DROP TABLE users;`},
}

func RunMigrations(db *gorm.DB) error {
    // 从文件中读取并按版本号顺序执行
    return nil
}

💡 最佳实践:开发环境用 AutoMigrate 快速迭代;测试和生产环境用声明式迁移工具(Atlas/golang-migrate)管理版本化的 SQL 迁移脚本。


迁移最佳实践

原则 说明
不逆向修改已有迁移 已部署的迁移文件不可修改,新变更用新迁移
每次迁移包含 Down 可回退的迁移才是完整的迁移
迁移前备份 生产环境迁移前务必备份数据库
避免大表锁表 MySQL 中 ALTER TABLE 可能锁全表(用 pt-online-schema-changegh-ost
创建索引要小心 MySQL 5.6 以下创建索引会锁表,5.6+ 支持 Online DDL
不带默认值的 NOT NULL 添加 NOT NULL 列时必须指定默认值,否则已有行的值不确定

🚨 陷阱:永远不要在生产环境直接使用 db.AutoMigrate()——它不支持修改列类型,也无法追踪版本变更。AutoMigrate 仅适用于开发环境。


常见陷阱汇总

陷阱 说明
🚨 AutoMigrate 不改列类型 修改模型字段类型后,数据库列不会自动变更
🚨 AutoMigrate 不删列 删除模型字段不会删除数据库列,造成"僵尸列"
🚨 物理外键风险 物理外键降低写入性能、阻碍数据迁移(生产环境大多禁用)
🚨 PostgreSQL 索引名冲突 一个数据库内索引名必须唯一(MySQL 是表内唯一)
🚨 大表索引创建 生产环境大表创建索引需评估 Online DDL 支持
🚨 忽略列类型差异 MySQL TINYINT(1) 映射到 Go bool,改成 TINYINT(4) 需手动迁移