第08章:表名约定与迁移详解
本章目标
学完本章后,你将能够:
- 完全理解 GORM 的表名生成规则
- 使用三种方式自定义表名
- 理解
AutoMigrate()的能力边界(能做什么、不能做什么) - 使用
Migrator()接口进行高级迁移操作 - 制定生产环境的数据库迁移策略
前置知识
- 需要先学习:第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 等)
练习题
- GORM 中
UserProfile、APIResponse、HTTPSConfig分别对应什么默认表名? - 写三种不同的方式将
User模型的表名设置为backend_users。 - 写一个迁移脚本:检查 users 表是否有
phone列,没有则添加,然后检查是否有idx_users_phone索引,没有则创建。 - AutoMigrate 能做什么?不能做什么?为什么不能做"减法"?
- 以下代码执行后,数据库表结构会发生什么变化?go
type User struct { ID uint; Name string } db.AutoMigrate(&User{}) // 第一次 type User struct { ID uint; Email string } db.AutoMigrate(&User{}) // 第二次 - (思考题)如果你的线上数据库有 1000 万条数据,你需要加一个新列,你会怎么做?直接用 AutoMigrate 会有风险吗?