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

12 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
表名
TableName
NamingStrategy
分表
2026-05-05 00:00

表名规则

概述

GORM 遵循"约定优于配置"原则,默认将 struct 名自动推导为蛇形复数的表名。当默认规则不满足需求时,可以通过 TableName() 方法、db.Table() 链式调用或 NamingStrategy 全局配置来自定义表名。本文将逐层拆解这三种方式的优先级、适用场景和常见陷阱。

GORM 表名推导的三层优先级

GORM 决定「这个 struct 对应哪张表」时,按照从高到低的优先级依次判断,一旦命中就停止后续推断:

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)对英语单词有内置规则,但中文拼音缩写或非标准单词可能产生意料之外的结果:

type UserTOTP struct { ... }  // → user_totps  ❌ 你可能期待 user_totp
type WechatUserInfo struct{}  // → wechat_user_infos(info 的复数规则不适用)
type AdminDatum struct { ... }  // → admin_data ✅ 但容易忘记 datum → data

如果你对表名有洁癖,建议直接实现 TableName() 来锁定表名,不要在复数规则上较劲。

如何关闭复数? 在连接数据库时配置:

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    NamingStrategy: schema.NamingStrategy{
        SingularTable: true, // 禁用表名复数,User → user
    },
})

第二层:TableName()——模型级别约定

当 struct 实现了 TableName() string 方法,GORM 优先使用其返回值。这是模型级别的约定,定义一次,全局生效。

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

// ✅ 推荐: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 是最自然的选择。

多环境动态表名

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")——链式调用,临时覆盖

这是查询级别的覆盖,不影响模型定义本身,只在当前链式调用中生效:

// 场景一:临时查一张视图
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():

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,         // 默认转为小写
    },
})

完整处理流程:

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_

这是最常见的场景,一次配置,全局生效:

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 无需任何修改:

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 连接下映射到不同物理表:

// 系统库连接:表前缀 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 可以组合使用,处理顺序参考上方流程图——先替换,再转蛇形,最后加前缀:

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():

// 全局 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,是有意为之的设计——模型级别的约定应该优先于全局配置。

决策指南

根据你的实际需求选择对应方案:

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

关联笔记