3. GORM 模型定义:结构体标签详解、字段权限控制、关联关系定义、表名与列名约定、时间戳字段
GORM 的模型定义是 ORM 映射的基石。通过 Go 结构体与标签(Tag),开发者可以精确控制数据库表结构、字段行为、关联关系以及迁移时的默认规则。本章将深入剖析模型定义的每一个核心环节,帮助你在实际项目中写出清晰、高效且可维护的模型代码。
3.1 结构体标签详解
GORM 使用结构体标签(Tag)来定义字段的数据库映射规则。标签格式为 gorm:"key1:value1;key2:value2"。以下是常用标签的完整清单:
| 标签键 | 说明 | 示例 |
|---|---|---|
column |
指定数据库列名 | column:user_name |
type |
指定列的数据类型(如 varchar(255)、text、bigint) | type:varchar(100) |
size |
指定列大小(仅对整型、字符串等有效) | size:256 |
primaryKey |
标记为主键 | primaryKey |
unique |
标记为唯一约束 | unique |
default |
指定默认值 | default:0 或 default:'active' |
not null |
标记为非空 | not null |
autoIncrement |
标记为自增 | autoIncrement |
index |
创建索引(可指定索引名) | index:idx_name |
uniqueIndex |
创建唯一索引 | uniqueIndex:uidx_email |
embedded |
嵌入结构体,将字段平铺到父表 | embedded |
embeddedPrefix |
嵌入结构体时添加列名前缀 | embeddedPrefix:addr_ |
comment |
添加列注释 | comment:用户邮箱 |
serializer |
指定序列化方式(json、gob、unixtime 等) | serializer:json |
最佳实践: 优先使用
column 显式指定列名,避免依赖 GORM 默认的蛇形命名转换,尤其是在团队协作或已有数据库表结构的情况下。
3.2 字段权限控制
GORM 允许通过标签控制字段在写入(Create/Update)和读取(Select)时的行为,实现细粒度的权限管理。这是防止数据泄露和误写入的关键手段。
- 写入权限控制: 使用
->表示只读(不可写入),<-表示只写(不可读取)。 - 创建/更新/查询控制: 使用
create、update、select标签,值为false时禁用对应操作。
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"column:user_name;not null"`
Password string `gorm:"->:false;<-:create"` // 只写(创建时写入),不可读取
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Role string `gorm:"default:user;select:false"` // 查询时忽略该字段
}
| 标签 | 含义 | 使用场景 |
|---|---|---|
-> |
只读字段,不可写入 | 计算字段、数据库触发器生成的字段 |
<- |
只写字段,不可读取 | 密码哈希、敏感令牌 |
<-:create |
仅创建时可写入 | 创建时间、初始状态 |
<-:update |
仅更新时可写入 | 最后修改人、版本号 |
select:false |
查询时忽略该字段 | 内部标记、大文本字段 |
注意: 权限控制标签仅影响 GORM 的 CRUD 方法(如
Create、Updates、Select)。如果使用原生 SQL 或 Raw 查询,这些标签不会生效,需要额外注意安全边界。
3.3 关联关系定义
GORM 支持四种核心关联关系:Belongs To、Has One、Has Many、Many To Many。通过结构体嵌套和标签,可以清晰表达表之间的外键与引用逻辑。
关联关系速查: 定义关联时,务必明确“谁拥有外键”。
Belongs To 的模型持有外键,Has One/Has Many 的关联模型持有外键。
// Belongs To: User 属于 Company(User 表持有 company_id 外键)
type User struct {
gorm.Model
Name string
CompanyID uint
Company Company `gorm:"foreignKey:CompanyID"`
}
// Has Many: Company 拥有多个 User(User 表持有 company_id 外键)
type Company struct {
gorm.Model
Name string
Users []User `gorm:"foreignKey:CompanyID"`
}
// Many To Many: User 与 Language 多对多(自动创建中间表 user_languages)
type User struct {
gorm.Model
Name string
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
Users []User `gorm:"many2many:user_languages;"`
}
3.4 表名与列名约定
GORM 默认使用 蛇形命名法(Snake Case)将结构体名和字段名转换为数据库表名和列名。开发者可以通过多种方式自定义这些映射。
3.4.1 默认转换规则
| Go 结构体/字段 | 默认表名/列名 | 说明 |
|---|---|---|
User |
users |
结构体名复数化 + 蛇形 |
OrderItem |
order_items |
驼峰转蛇形 + 复数 |
UserName |
user_name |
驼峰转蛇形 |
ID |
id |
默认主键列名 |
3.4.2 自定义表名与列名
- 表名自定义: 实现
TableName()接口,返回自定义表名。 - 列名自定义: 使用
column标签显式指定。 - 全局命名策略: 通过
NamingStrategy配置,可自定义表前缀、单数表名等。
// 自定义表名
func (User) TableName() string {
return "t_user"
}
// 全局命名策略配置
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_", // 表名前缀
SingularTable: true, // 使用单数表名
NoLowerCase: false, // 是否强制小写
},
})
建议: 在已有数据库的项目中,始终显式实现
TableName() 并使用 column 标签,避免因默认命名规则与现有表结构不匹配导致的迁移错误。
3.5 时间戳字段
GORM 内置了对 CreatedAt、UpdatedAt、DeletedAt 三个时间戳字段的自动管理。这些字段在模型定义中具有特殊行为,无需手动赋值。
| 字段名 | 类型 | 自动行为 |
|---|---|---|
CreatedAt |
time.Time |
创建记录时自动写入当前时间 |
UpdatedAt |
time.Time |
创建和更新记录时自动更新为当前时间 |
DeletedAt |
gorm.DeletedAt |
软删除时写入删除时间,查询自动过滤 |
3.5.1 自定义时间戳字段名与行为
如果不想使用默认的字段名,可以通过标签 autoCreateTime、autoUpdateTime、autoDeleteTime 来指定。
type Order struct {
ID uint `gorm:"primaryKey"`
OrderNo string `gorm:"uniqueIndex"`
CreatedOn time.Time `gorm:"autoCreateTime"` // 自动创建时间
UpdatedOn time.Time `gorm:"autoUpdateTime"` // 自动更新时间
DeletedOn gorm.DeletedAt `gorm:"autoDeleteTime;index"` // 自动软删除时间
PaidAt *time.Time `gorm:"default:null"` // 手动管理的时间字段
}
3.5.2 时间精度与类型控制
GORM 支持通过 type 标签控制时间字段的数据库类型,例如使用 datetime(3) 存储毫秒精度。
type Event struct {
ID uint `gorm:"primaryKey"`
Name string
StartTime time.Time `gorm:"type:datetime(3);not null"` // 毫秒精度
CreatedAt time.Time `gorm:"autoCreateTime;type:date"` // 仅存储日期
}
注意: 使用
gorm.DeletedAt 时,GORM 会自动在查询中添加 WHERE deleted_at IS NULL 条件。如果使用 Unscoped() 方法可以查询到软删除的记录。务必确保软删除字段有索引,否则全表扫描会影响性能。
3.6 模型定义完整示例
以下是一个综合了标签、权限控制、关联关系、自定义表名和时间戳的完整模型示例:
type Product struct {
ID uint `gorm:"primaryKey;autoIncrement;comment:产品ID"`
Name string `gorm:"type:varchar(200);not null;uniqueIndex:uidx_name;comment:产品名称"`
Description string `gorm:"type:text;comment:产品描述"`
Price float64 `gorm:"type:decimal(10,2);not null;default:0.00"`
CategoryID uint `gorm:"not null;index"`
Category Category `gorm:"foreignKey:CategoryID"`
Tags []Tag `gorm:"many2many:product_tags;"`
CreatedAt time.Time `gorm:"autoCreateTime;type:datetime(3)"`
UpdatedAt time.Time `gorm:"autoUpdateTime;type:datetime(3)"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:软删除时间"`
InternalID string `gorm:"->:false;<-:create;comment:内部ID,仅创建时写入"`
}
func (Product) TableName() string {
return "t_product"
}
type Category struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(100);not null;unique"`
}
type Tag struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;unique"`
}
迁移建议: 在定义模型时,尽量将
comment 标签写完整。这些注释会通过 AutoMigrate 写入数据库,极大提升数据库的可维护性,也方便后续 DBA 审查。