Skip to content

第08章:表名约定与迁移详解 ​

本章目标 ​

学完本章后,你将能够:

  1. 完全理解 GORM 的表名生成规则
  2. 使用三种方式自定义表名
  3. 理解 AutoMigrate() 的能力边界(能做什么、不能做什么)
  4. 使用 Migrator() 接口进行高级迁移操作
  5. 制定生产环境的数据库迁移策略

前置知识 ​

  • 需要先学习:第05-07章(模型定义)
  • 需要了解:MySQL DDL 操作(CREATE TABLE、ALTER TABLE)

8.1 表名生成规则详解 ​

默认规则 ​

GORM 默认的表名生成规则是:结构体名 → 蛇形(snake_case)→ 复数

User           → users
Product        → products
ProductCategory → product_categories
APIUser        → api_users
UserProfile    → user_profiles

转换过程:

User → 蛇形 → user → 复数 → users
ProductCategory → 蛇形 → product_category → 复数 → product_categories

规则的例外 ​

有些单词的复数不是简单加 s,GORM 内置了常见不规则复数:

go
type Person struct{}        // → people
type Child struct{}         // → children
type Mouse struct{}         // → mice
type Octopus struct{}       // → octopi

查看实际表名 ​

go
// 方法一:用 Statement 查看
stmt := &gorm.Statement{DB: db}
stmt.Parse(&User{})
fmt.Println(stmt.Schema.Table)  // 输出:users

// 方法二:直接看 SQL 日志
db.AutoMigrate(&User{})
// 日志中可以看到:CREATE TABLE `users` (...)

8.2 三种自定义表名方式 ​

方式一:TableName() 方法(推荐 ✅) ​

直接在模型上定义 TableName() 方法:

go
type User struct {
	ID   uint
	Name string
}

// 自定义表名(优先级最高!)
func (User) TableName() string {
	return "sys_users"
}

之后所有的 GORM 操作都会自动使用 sys_users:

go
db.AutoMigrate(&User{})        // → CREATE TABLE `sys_users` (...)
db.Create(&User{Name: "张三"}) // → INSERT INTO `sys_users` ...
db.Find(&users)                // → SELECT * FROM `sys_users`

📌 推荐使用 TableName() 方式:明确、不会被覆盖、IDE 可以跳转。唯一缺点是每张表都要写一次方法。

方式二:db.Table() 临时指定 ​

go
// 仅这一次查询使用指定的表名
db.Table("archive_users").Find(&users)
db.Table("temp_data").Create(&record)

适用场景:

  • 操作临时表
  • 操作归档表(如按月分表:orders_202401)
  • 一张结构体对应多张结构相同的表

方式三:NamingStrategy 全局配置 ​

go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
	NamingStrategy: schema.NamingStrategy{
		TablePrefix:   "t_",    // 所有表加前缀
		SingularTable: true,    // 禁用复数
		NoLowerCase:   false,   // 启用小写(默认)
	},
})

效果:

go
// User → t_user(有前缀,单数)
// Product → t_product
// ProductCategory → t_product_category

三种方式的优先级 ​

TableName() 方法  >  db.Table() 临时指定  >  NamingStrategy 全局配置  >  默认规则
    (最高)              (中等)              (低)                (最低)

8.3 AutoMigrate() 详解 ​

AutoMigrate 能做什么 ​

go
db.AutoMigrate(&User{}, &Product{}, &Order{})
操作说明示例
✅ 创建表表不存在时创建CREATE TABLE users (...)
✅ 添加列结构体新增字段 → 表新增列ALTER TABLE users ADD COLUMN phone VARCHAR(20)
✅ 添加索引新增的 index/uniqueIndex 标签CREATE INDEX idx_... ON users (...)
✅ 修改列类型标签 type 变化时尝试修改(有限)ALTER TABLE users MODIFY COLUMN ...
✅ 创建外键关联标签对应的外键(默认开启)ALTER TABLE users ADD CONSTRAINT ...

AutoMigrate 不能做什么 ​

操作为什么不支持风险
❌ 删除列防止数据丢失你想删,但 GORM 不敢删
❌ 修改列名无法判断是"改名"还是"删了旧的建了新的"误删列,数据丢失
❌ 删除索引同删除列的原因误删索引
❌ 修改表名同理误删表
❌ 删除外键同理误删外键约束

⚠️ 核心原则:AutoMigrate 只做"加法"(添加),不做"减法"(删除)。这是出于数据安全的设计——删列意味着数据永久丢失,GORM 不敢替你冒这个险。

实战验证 ​

go
// 第一版:只有 Name 字段
type User struct {
	ID   uint
	Name string
}
db.AutoMigrate(&User{})  // → CREATE TABLE users (id, name)

// 第二版:加了 Email 字段
type User struct {
	ID    uint
	Name  string
	Email string  // 新增
}
db.AutoMigrate(&User{})  // → ALTER TABLE users ADD COLUMN email VARCHAR(255)

// 第三版:去掉了 Name 字段
type User struct {
	ID    uint
	Email string
}
db.AutoMigrate(&User{})  // → 什么都不做!name 列还在表中!

// 第四版:又把 Name 改名为 UserName
type User struct {
	ID       uint
	Email    string
	UserName string  // 对 GORM 来说是新字段
}
db.AutoMigrate(&User{})  // → ALTER TABLE users ADD COLUMN user_name VARCHAR(255)
// ⚠️ 表现在有 name 和 user_name 两列!

8.4 Migrator() 接口:手动管理迁移 ​

当 AutoMigrate 不够用时,使用 Migrator() 手动管理:

表操作 ​

go
migrator := db.Migrator()

// 检查表是否存在
if migrator.HasTable(&User{}) {
	fmt.Println("users 表存在")
}

// 检查表是否存在(按表名)
if migrator.HasTable("users") {
	fmt.Println("users 表存在")
}

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

// 重命名表
migrator.RenameTable(&User{}, "archived_users")
migrator.RenameTable("users", "archived_users")

// 删除表(危险!)
migrator.DropTable(&User{})
migrator.DropTable("users")

// 检查列是否存在
if migrator.HasColumn(&User{}, "email") {
	fmt.Println("email 列存在")
}

列操作 ​

go
// 添加列
migrator.AddColumn(&User{}, "Phone")

// 删除列(危险!会丢失数据!)
migrator.DropColumn(&User{}, "temp_field")

// 修改列类型
migrator.AlterColumn(&User{}, "age")

// 重命名列
migrator.RenameColumn(&User{}, "old_name", "new_name")

索引操作 ​

go
// 检查索引是否存在
migrator.HasIndex(&User{}, "idx_users_email")

// 创建索引
migrator.CreateIndex(&User{}, "idx_users_email")

// 删除索引
migrator.DropIndex(&User{}, "idx_users_email")

// 重命名索引
migrator.RenameIndex(&User{}, "old_idx", "new_idx")

完整的迁移脚本示例 ​

go
func runMigration(db *gorm.DB) error {
	m := db.Migrator()

	// 1. 检查表是否存在
	if !m.HasTable(&User{}) {
		// 2. 不存在就创建
		if err := m.CreateTable(&User{}); err != nil {
			return err
		}
		fmt.Println("✅ users 表创建成功")
	} else {
		fmt.Println("⏭️  users 表已存在,跳过")
	}

	// 3. 检查并添加新列
	if !m.HasColumn(&User{}, "phone") {
		if err := m.AddColumn(&User{}, "Phone"); err != nil {
			return err
		}
		fmt.Println("✅ phone 列添加成功")
	}

	// 4. 检查并创建索引
	if !m.HasIndex(&User{}, "idx_users_phone") {
		if err := m.CreateIndex(&User{}, "idx_users_phone"); err != nil {
			return err
		}
		fmt.Println("✅ idx_users_phone 索引创建成功")
	}

	return nil
}

8.5 迁移策略对比 ​

策略一:AutoMigrate 一把梭(小项目/开发环境) ​

go
// 启动时自动迁移
db.AutoMigrate(&User{}, &Product{}, &Order{})

优点:简单,零配置,表结构永远和模型一致(增量) 缺点:不可控,不知道具体执行了什么 DDL,生产环境有风险

策略二:Migrator + 迁移脚本(中大型项目) ​

go
// 每次表结构变更写一个迁移函数
func migrateV001(db *gorm.DB) error { ... }  // 初始化表
func migrateV002(db *gorm.DB) error { ... }  // 加 phone 列
func migrateV003(db *gorm.DB) error { ... }  // 加索引

优点:可控、可回滚、有变更记录 缺点:需要手动维护

策略三:专业迁移工具(企业级项目) ​

使用 golang-migrate、goose、atlas 等专业工具:

bash
# 使用 golang-migrate
migrate create -ext sql -dir migrations -seq add_phone_column

优点:版本化管理、支持回滚、与语言无关 缺点:学习成本、需要额外工具

策略选择建议 ​

项目规模推荐策略
个人学习/原型AutoMigrate
小型项目(1-5张表)AutoMigrate + TableName()
中型项目(10+张表)Migrator() + 迁移脚本
大型/企业项目golang-migrate 等专业工具

常见错误 ​

错误1:以为 AutoMigrate 会删除不用的列 ​

go
// 第一版
type User struct { ID uint; Name string }
db.AutoMigrate(&User{})

// 第二版:删掉了 Name
type User struct { ID uint }
db.AutoMigrate(&User{})
// ❌ name 列还在!GORM 不会删除它

解决:手动用 Migrator() 删除不需要的列。

错误2:生产环境用 AutoMigrate ​

go
// ❌ 生产环境直接 AutoMigrate
db.AutoMigrate(&User{}, &Product{})

// 风险:
// 1. DDL 操作可能导致锁表
// 2. 不知道具体执行了什么变更
// 3. 失败后难以回滚

解决:生产环境禁用 AutoMigrate,使用专业迁移工具。

错误3:表名改了但忘了改 TableName() ​

go
// 重构:User → Customer
type Customer struct { ... }

func (Customer) TableName() string {
	return "users"  // ⚠️ 还是旧表名?容易混淆
}

// ✅ 重构表名
func (Customer) TableName() string {
	return "customers"
}

错误4:db.Table() 之后忘记 Model() ​

go
// ❌ 指定了表名但没指定模型
db.Table("users").Create(&data)
// GORM 不知道用哪个结构体的字段信息

// ✅ 同时指定表名和模型
db.Model(&User{}).Table("archive_users").Find(&users)

本章小结 ​

  • 表名生成规则:结构体名 → 蛇形 → 复数
  • 自定义表名三种方式:TableName() 方法 > db.Table() > NamingStrategy
  • AutoMigrate 只做加法不做减法,生产环境慎用
  • Migrator() 提供完整的 DDL 操作能力
  • 大项目应使用专业迁移工具(golang-migrate 等)

练习题 ​

  1. GORM 中 UserProfile、APIResponse、HTTPSConfig 分别对应什么默认表名?
  2. 写三种不同的方式将 User 模型的表名设置为 backend_users。
  3. 写一个迁移脚本:检查 users 表是否有 phone 列,没有则添加,然后检查是否有 idx_users_phone 索引,没有则创建。
  4. AutoMigrate 能做什么?不能做什么?为什么不能做"减法"?
  5. 以下代码执行后,数据库表结构会发生什么变化?
    go
    type User struct { ID uint; Name string }
    db.AutoMigrate(&User{})  // 第一次
    type User struct { ID uint; Email string }
    db.AutoMigrate(&User{})  // 第二次
  6. (思考题)如果你的线上数据库有 1000 万条数据,你需要加一个新列,你会怎么做?直接用 AutoMigrate 会有风险吗?

把零散知识,整理成可以反复走的路。