vault backup: 2026-05-05 20:22:48
This commit is contained in:
+3
-32
@@ -30,39 +30,10 @@ type User struct {
|
|||||||
|
|
||||||
## 表名规则
|
## 表名规则
|
||||||
|
|
||||||
### 默认命名策略
|
GORM 通过三层优先级推导表名:`db.Table()` → `TableName()` 方法 → struct 名蛇形复数。自定义表名时还涉及 `NamingStrategy` 全局配置、value/pointer receiver 选择、分表策略等细节——详见子文档:
|
||||||
|
|
||||||
GORM 使用 `Table` 方法来推导表名:
|
> [!abstract] 子文档
|
||||||
|
> - [[02-模型定义/表名规则]] — 默认策略、TableName()、db.Table()、NamingStrategy、分表实战、决策指南
|
||||||
| 代码行为 | 结果 |
|
|
||||||
|----------|------|
|
|
||||||
| 默认 | struct 名的蛇形复数形式(`User` → `users`) |
|
|
||||||
| 实现 `TableName() string` | 返回的字符串 |
|
|
||||||
| 使用 `db.Table("xxx")` | 本次查询使用的表名(不会修改 Model 的 TableName) |
|
|
||||||
|
|
||||||
> [!tip] `TableName()` vs `db.Table()`:何时用哪个?
|
|
||||||
> - **结构性差异**(多环境、分库分表)→ 用 `TableName()`,因为这是模型级别的约定。
|
|
||||||
> - **临时覆盖**(一个复杂查询需要 JOIN 不同视图)→ 用 `db.Table()`,它只影响当前链式调用,不污染模型定义。
|
|
||||||
|
|
||||||
```go
|
|
||||||
func (User) TableName() string {
|
|
||||||
return "sys_user" // 显式指定表名;value receiver 即可,无需接收者变量
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> [!note] 为什么用 value receiver `(User)` 而非 `*User`?
|
|
||||||
> `TableName()` 只需要返回一个字符串,不需要访问任何字段。GORM 内部会先尝试值接收者方法,找不到再尝试指针接收者——所以用 `(User)` 既能被值对象调用也能被指针对象调用,灵活性更好。
|
|
||||||
|
|
||||||
> [!tip] 多环境表名
|
|
||||||
> 不同环境下表名前缀不同?可以用环境变量 + `TableName()` 实现动态切换:
|
|
||||||
> ```go
|
|
||||||
> func (User) TableName() string {
|
|
||||||
> if os.Getenv("ENV") == "test" {
|
|
||||||
> return "test_sys_user"
|
|
||||||
> }
|
|
||||||
> return "sys_user"
|
|
||||||
> }
|
|
||||||
> ```
|
|
||||||
|
|
||||||
## 字段 Tag 详解
|
## 字段 Tag 详解
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
---
|
||||||
|
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-安装与初始化]]
|
||||||
Reference in New Issue
Block a user