迁移与索引
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-change 或 gh-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) 需手动迁移 |