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;"`
}
GORM 四种关联关系结构图 User Company Belongs To (外键在 User) User Profile Has One (外键在 Profile) Company Users [] Has Many (外键在 User) User Language user_languages Many To Many (中间表)

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() 方法可以查询到软删除的记录。务必确保软删除字段有索引,否则全表扫描会影响性能。
GORM 时间戳字段生命周期 Create CreatedAt = now UpdatedAt = now Update UpdatedAt = now Soft Delete DeletedAt = now 自动管理规则 CreatedAt 仅在创建时写入,UpdatedAt 每次更新自动刷新 DeletedAt 非空时,GORM 自动过滤该记录(软删除)

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 审查。