Files
cs-note/hhs/GORM/02-模型定义/表名规则.md
T
2026-05-24 11:42:38 +08:00

322 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [GORM, Go, ORM, 表名, TableName, NamingStrategy, 分表]
create time: 2026-05-05 00:00
---
# 表名规则
## 概述
GORM 遵循"约定优于配置"原则,默认将 struct 名自动推导为蛇形复数的表名。当默认规则不满足需求时,可以通过 `TableName()` 方法、`db.Table()` 链式调用或 `NamingStrategy` 全局配置来自定义表名。本文将逐层拆解这三种方式的优先级、适用场景和常见陷阱。
## GORM 表名推导的三层优先级
GORM 决定「这个 struct 对应哪张表」时,按照从高到低的优先级依次判断,一旦命中就停止后续推断:
```mermaid
graph TD
A["查询/操作开始"] --> B{"db.Table() 是否被调用?"}
B -->|"是"| C["使用 db.Table() 指定的表名<br/>绕过所有其他规则"]
B -->|"否"| D{"Model 是否实现<br/>TableName() 方法?"}
D -->|"是"| E["使用 TableName() 返回值<br/>绕过 NamingStrategy"]
D -->|"否"| F["应用 NamingStrategy<br/>struct名 → 蛇形 → 复数 → 前缀"]
C --> G["生成 SQL"]
E --> G
F --> G
style C fill:#EF4444,color:#fff
style E fill:#F59E0B,color:#fff
style F fill:#3B82F6,color:#fff
style G fill:#10B981,color:#fff
```
> [!important] 关键结论
> `TableName()` 的返回值会**绕过** `NamingStrategy` 配置。如果你设置了 `TablePrefix: "t_"`,但 `TableName()` 返回 `"sys_user"`,最终表名仍是 `"sys_user"` 而非 `"t_sys_user"`。**要么全用 `TableName()` 自己管理,要么全依赖 `NamingStrategy`**,混用会导致表名前缀不一致。
### 第一层:默认策略——蛇形复数(零配置)
这是 GORM 的开箱即用默认行为。struct 名经过两个转换步骤:
```
struct 名 → 蛇形(snake_case) → 复数(plural)
```
| Struct 名 | 推导表名 | 规则说明 |
|-----------|---------|---------|
| `User` | `users` | 蛇形为 `user`,加复数 `s` |
| `UserProfile` | `user_profiles` | 大驼峰分拆为 `user` + `profile`,加复数 |
| `APIKey` | `api_keys` | 连续大写视为一个词:`API` → `api` |
| `OrderItem` | `order_items` | 两词分别转蛇形后拼接 |
| `Category` | `categories` | 以 `y` 结尾,变 `y` 为 `ies` |
| `Box` | `boxes` | 以 `x` 结尾,加 `es` |
| `AdminUser` | `admin_users` | 标准英语单词,复数规则正确 |
> [!warning] 复数的坑
> GORM 使用的复数引擎(`jinzhu/inflection`)对英语单词有内置规则,但**中文拼音缩写**或**非标准单词**可能产生意料之外的结果:
>
> ```go
> type UserTOTP struct { ... } // → user_totps ❌ 你可能期待 user_totp
> type WechatUserInfo struct{} // → wechat_user_infos(info 的复数规则不适用)
> type AdminDatum struct { ... } // → admin_data ✅ 但容易忘记 datum → data
> ```
>
> 如果你对表名有洁癖,建议直接实现 `TableName()` 来锁定表名,不要在复数规则上较劲。
**如何关闭复数?** 在连接数据库时配置:
```go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 禁用表名复数,User → user
},
})
```
### 第二层:`TableName()`——模型级别约定
当 struct 实现了 `TableName() string` 方法,GORM 优先使用其返回值。这是**模型级别的约定**,定义一次,全局生效。
```go
func (User) TableName() string {
return "sys_user" // 显式指定表名
}
```
**适用场景**:
| 场景 | 示例 | 说明 |
|------|------|------|
| 表名与模型名不同 | `User` → `sys_user` | 遗留系统或统一命名规范 |
| 多环境前缀 | 测试环境 `test_user`,生产 `user` | 通过环境变量动态切换 |
| 分库分表 | `order_2026_05` 按月份分表 | 拼接时间维度 |
| 多租户 | `t001_users`、`t002_users` | 拼接租户 ID |
> [!question] 思考
> 如果 `User` 没有主键字段,也没有实现 `TableName()`,GORM 不把它当作 Model。但如果只实现了 `TableName()` 而没有主键,它算 Model 吗?
>
> **答**:算。Model 的三个条件(有主键 / 实现 `TableName()` / 被 GORM 方法引用)是**或**的关系,满足其一即可。不过没有主键时 `First()`、`Take()` 等依赖主键的方法无法正常工作。
#### value receiver vs pointer receiver
```go
// ✅ 推荐:value receiver
func (User) TableName() string { return "sys_user" }
// ⚠️ 可用但不够灵活
func (*User) TableName() string { return "sys_user" }
```
GORM 内部的查找逻辑是先尝试值接收者的方法集,找不到再尝试指针接收者。用 `(User)` 时:
- `db.Create(&user)` → 指针可以访问值接收者方法 ✅
- `db.Create(user)` → 值也可以访问值接收者方法 ✅
而用 `(*User)` 时,如果某处代码传递的是**值**而非指针,就无法访问该方法。另外 `TableName()` 只是返回一个字符串常量,不需要修改任何字段,value receiver 是最自然的选择。
#### 多环境动态表名
```go
func (User) TableName() string {
switch os.Getenv("APP_ENV") {
case "test":
return "test_sys_user"
case "staging":
return "staging_sys_user"
default:
return "sys_user"
}
}
```
> [!tip] 环境区分用 `TableName()` vs `NamingStrategy`?
> - 少数几个表的表名需要区分环境 → 用 `TableName()` + 环境变量(更灵活)
> - 所有表统一加前缀 → 用 `NamingStrategy.TablePrefix`(更简洁)
### 第三层:`db.Table("xxx")`——链式调用,临时覆盖
这是**查询级别**的覆盖,不影响模型定义本身,只在当前链式调用中生效:
```go
// 场景一:临时查一张视图
db.Table("user_view").Where("active = ?", true).Find(&users)
// 场景二:按月份分表查询
tableName := fmt.Sprintf("orders_%s", time.Now().Format("2006_01"))
db.Table(tableName).Create(&order)
// 本次调用后,User 的默认表名仍然是它原来的样子
db.Find(&users) // 走默认表名,不受上面 db.Table() 影响
```
> [!tip] 分表场景推荐用 `db.Table()` 而非 `TableName()`
> `TableName()` 在**每次**操作时都会调用。如果你在 `TableName()` 里用 `time.Now()` 动态拼接,可能导致同一个事务中前后不一致——INSERT 和 UPDATE 跨月边界时落到不同表。推荐在业务层显式传入时间或分表键,用 `db.Table()` 明确指定。
## 全局表名策略:NamingStrategy
如果你想**统一**管理所有表的命名规则(前缀、后缀、单复数、大小写),不需要每个 struct 写一遍 `TableName()`:
```go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_", // 所有表加 t_ 前缀 → t_users
SingularTable: true, // 不加复数 → t_user
NameReplacer: strings.NewReplacer("sys_", "system_"), // 自定义替换
NoLowerCase: false, // 默认转为小写
},
})
```
**完整处理流程**:
```mermaid
graph LR
A["struct 名: UserProfile"] --> B["NameReplacer 替换"]
B --> C["转蛇形: user_profile"]
C --> D{"SingularTable?"}
D -->|"false(默认)"| E["加复数: user_profiles"]
D -->|"true"| F["不加: user_profile"]
E --> G["加前缀: t_user_profiles"]
F --> G
G --> H["最终表名"]
```
### TablePrefix 实战案例
#### 案例一:基础前缀——所有表加 `t_`
这是最常见的场景,一次配置,全局生效:
```go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_", // User → t_users, OrderItem → t_order_items
SingularTable: false, // 默认值,可省略
},
})
```
> [!tip] 命名习惯参考
> - `t_`:table 的缩写,最通用
> - `tb_`:table 的变体缩写,部分团队习惯
> - `tbl_`:另一种常见写法
> - 保持团队统一即可,无优劣之分
#### 案例二:按环境切换前缀
通过环境变量或配置文件,在不同环境下使用不同前缀。**所有 struct 无需任何修改**:
```go
func getTablePrefix() string {
switch os.Getenv("APP_ENV") {
case "dev":
return "dev_"
case "test":
return "test_"
default:
return "" // 生产环境无前缀
}
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: getTablePrefix(), // 测试环境 → test_user,生产环境 → user
SingularTable: true,
},
})
```
> [!tip] 环境区分用 `TableName()` vs `TablePrefix`?
> - 少数几个表的表名需要区分环境 → 用 `TableName()` + 环境变量(更灵活)
> - 所有表统一加前缀 → 用 `NamingStrategy.TablePrefix`(更简洁)
#### 案例三:多数据库连接各自前缀
当项目连接多个数据库时,为每个连接配置不同的前缀。**同名的 struct,在不同 DB 连接下映射到不同物理表**:
```go
// 系统库连接:表前缀 sys_
sysDB, _ := gorm.Open(mysql.Open(sysDSN), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "sys_",
SingularTable: true,
},
})
// 业务库连接:表前缀 biz_
bizDB, _ := gorm.Open(mysql.Open(bizDSN), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "biz_",
SingularTable: true,
},
})
// 同一个 User struct,在不同 DB 连接下映射到不同表
sysDB.Find(&users) // → SELECT * FROM `sys_user`
bizDB.Find(&users) // → SELECT * FROM `biz_user`
```
复用同一套 struct 定义,只需在建立连接时区分前缀,避免了为每个库重复定义模型。
#### 案例四:前缀 + NameReplacer 组合
`TablePrefix` 和 `NameReplacer` 可以组合使用,处理顺序参考上方流程图——**先替换,再转蛇形,最后加前缀**:
```go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_",
SingularTable: true,
// SysUser → 替换为 SystemUser → 蛇形 system_user → 加前缀 → t_system_user
NameReplacer: strings.NewReplacer("sys_", "system_"),
},
})
```
> [!question] 思考:`db.Table()` 和 `TablePrefix` 同时存在会怎样?
> 如果你配置了 `TablePrefix: "t_"`,但链式调用 `db.Table("orders").Find(...)`,最终查哪张表?
>
> **答**:`orders`,不会自动加 `t_` 前缀。`db.Table()` 是第一优先级,传入什么表名就查什么表,**完全绕过** `NamingStrategy`。只有不调用 `db.Table()` 的普通 struct 操作才会命中 `TablePrefix`。
> [!warning] 混用 `TableName()` 和 `TablePrefix` 的坑
> 假设你全局配置了 `TablePrefix: "t_"`,但同时给部分 struct 实现了 `TableName()`:
>
> ```go
> // 全局 NamingStrategy 配置了 TablePrefix: "t_"
>
> type Product struct{} // 没有 TableName() → 自动生成 t_products ✅
>
> func (User) TableName() string {
> return "sys_user" // 显式返回,不会自动加前缀 → sys_user ❌
> }
> ```
>
> 结果:`Product` 查 `t_products`,`User` 查 `sys_user`——表名前缀不统一。**要么全用 `TableName()` 手动管理前缀,要么全依赖 `NamingStrategy.TablePrefix`,不要混用。**
> [!note] NamingStrategy 不作用的地方
> `NamingStrategy` 只影响**未实现 `TableName()`** 的 struct。一旦你为某个 struct 实现了 `TableName()`,GORM 会跳过 `NamingStrategy` 的所有规则直接使用返回值。这不是 bug,是有意为之的设计——模型级别的约定应该优先于全局配置。
## 决策指南
根据你的实际需求选择对应方案:
```mermaid
graph TD
Q["我需要自定义表名吗?"] -->|"不需要,默认就够"| A["零配置<br/>User → users"]
Q -->|"所有表统一加前缀"| B["NamingStrategy.TablePrefix"]
Q -->|"个别表名不符合默认规则"| C["实现 TableName()"]
Q -->|"临时查视图/分表"| D["db.Table()"]
Q -->|"按时间/租户分表"| E["db.Table() 动态拼接"]
Q -->|"关闭复数"| F["NamingStrategy.SingularTable"]
style A fill:#3B82F6,color:#fff
style B fill:#10B981,color:#fff
style C fill:#F59E0B,color:#fff
style D fill:#EF4444,color:#fff
style E fill:#EF4444,color:#fff
style F fill:#10B981,color:#fff
```
## 关联笔记
- [[../02-模型定义]]
- [[../../03-CRUD 操作]]
- [[../../01-安装与初始化]]