This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/GORM/02-模型定义.md
T
2026-05-05 21:21:58 +08:00

13 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
模型
struct tag
字段映射
主键
表名
2026-04-28 00:00

模型定义

概述

模型(Model)是 GORM 操作的核心入口——一个普通的 Go struct 被 GORM「看见」后,就可以直接进行数据库操作。本章讲解如何通过 struct tag、命名约定和接口来自定义 GORM 对模型的解读。

什么是 Model?

[!definition] Model Model 是一个包含以下任一条件的 struct:

  1. 有主键字段
  2. 定义了 TableName() 方法
  3. 被 AutoMigrate、Create、Where 等方法引用
type User struct {
    ID   uint   `gorm:"primaryKey"`
    Name string `gorm:"size:64;not null"`
}
// 这就是一个最简单的 GORM Model

[!tip] 核心原则 GORM 的本质是一个「约定优于配置」的 ORM。struct 只是普通的 Go 结构体,gorm:"..." tag 告诉 GORM 如何将它映射到数据库——不修改运行时行为,只在构建 SQL 和建表时生效。这意味着你可以将同一个 struct 同时用于 HTTP 请求体和数据库模型(通过暴露合适的字段)。

表名规则

GORM 通过三层优先级推导表名:db.Table() → TableName() 方法 → struct 名蛇形复数。自定义表名时还涉及 NamingStrategy 全局配置、value/pointer receiver 选择、分表策略等细节——详见子文档:

[!abstract] 子文档

字段 Tag 详解

完整 Tag 语法

gorm:"column:name;type:bigint;not null;default:0;uniqueIndex;index;comment:用户ID"
Tag 关键字 作用 示例
column 映射列名 column:user_id
type 列类型覆盖 type:varchar(128)
not null NOT NULL 约束 not null
default 默认值 default:'unknown'
primaryKey 主键 primaryKey
autoIncrement 自增 autoIncrement
uniqueIndex 唯一索引 uniqueIndex:idx_name
index 普通索引 index:idx_status
comment 注释(MySQL) comment:用户名
<- 字段写入控制 <-:true / <-:false / <-:create / <-:update
-> 读取控制(只读 = 只从 DB 读出) ->:true / ->:false(不读取回 struct)
<- + -> 组合控制读写 <-:false;->:true(只读,不可写入)
- 忽略此字段 -

写入权限控制

type Product struct {
    ID        uint      `gorm:"primaryKey;autoIncrement"`
    Name      string    `gorm:"not null;<-:create"`          // 仅创建时可写入
    Price     float64   `gorm:"not null;<-:true;->:true"`    // 读写均可(默认)
    CreatedAt time.Time `gorm:"<-:create;->:true;autoTime"`  // 仅创建时自动填充
    UpdatedAt time.Time `gorm:"<-:update;->:true;autoTime"`  // 仅更新时自动填充
    ViewCount int       `gorm:"->:false;<-:false"`           // 完全忽略(非 DB 字段)
}

[!example] 方向记忆法 <- 表示数据流向数据库(写入),-> 表示数据从数据库流出(读取)。箭头方向就是数据的方向。

嵌入 Struct(组合优于继承)

Go 不支持类的继承,但可以通过结构体嵌套实现代码复用:

// 方式一:显式嵌入公共字段
type AuditRecord struct {
    CreatedBy string `gorm:"size:64"`      // 创建人
    UpdatedBy string `gorm:"size:64"`      // 最后修改人
}

type User struct {
    ID   uint       `gorm:"primaryKey"`
    Name string     `gorm:"size:64;not null"`
    AuditRecord                 // 匿名嵌入——Go 将 AuditRecord 的字段提升为 User 的直接字段
}

// 方式二:使用 gorm.Model(推荐 🌟)
type Article struct {
    gorm.Model           // 自动包含 ID、CreatedAt、UpdatedAt、DeletedAt
    Title      string    `gorm:"size:128;not null"`
    Content    string    `gorm:"type:text"`
}

[!note] 为什么推荐 gorm.Model? gorm.Model 是 GORM 预定义的 struct,包含四个最常见字段:

type Model struct {
    ID        any         `gorm:"primaryKey"`                    // 主键,类型为 any (v2.27+)
    CreatedAt time.Time                                   // 创建时间
    UpdatedAt time.Time                                   // 更新时间
    DeletedAt gorm.DeletedAt                          // 软删除时间戳
}

嵌入后即可拥有完整的审计追踪 + 软删除能力,无需每个 struct 重复声明。

[!question] 思考 如果 User 同时嵌入了 AuditRecord 和 gorm.Model,而两者都有 CreatedBy 字段,会发生什么? 答:编译报错——Go 不允许歧义字段访问。此时应只保留一个来源,或改为普通成员变量而非嵌入:Audit AuditRecord。

字段类型映射

Go 类型 推荐 DB 类型 说明
int, int64 BIGINT GORM 默认以 int64 处理
uint, uint64 BIGINT UNSIGNED —
string VARCHAR(n) 需手动指定 size
bool TINYINT(1) / BOOLEAN —
float64 DOUBLE —
time.Time DATETIME / TIMESTAMP 配合 autoTime tag
[]byte BLOB / BYTEA 二进制数据
json.RawMessage JSON JSON 序列化字段

autoTime 自动时间戳

type Article struct {
    ID        uint      `gorm:"primaryKey"`
    Title     string    `gorm:"size:128;not null"`
    CreatedAt time.Time `gorm:"autoTime;createTime"`  // 首次插入时自动填入当前时间,后续 UPDATE 不会修改
    UpdatedAt time.Time `gorm:"autoTime;updateTime"`  // 每次 UPDATE 时自动更新为当前时间
}

[!tip] autoTime + createTime / updateTime 的分工

  • 只写 autoTime(旧写法):GORM 在 INSERT 和 UPDATE 时都更新时间字段。
  • autoTime + createTime:INSERT 时设置 CreatedAt,UPDATE 不碰它——这才是创建时间的正确语义。
  • autoTime + updateTime:UPDATE 时设置 UpdatedAt,INSERT 时同样会设初始值。

主键策略

默认策略:ID uint

type User struct {
    ID   uint   // GORM 默认将名为 ID 的字段视为主键
    Name string
}

UUID 主键

import "github.com/google/uuid"

type Order struct {
    ID   uuid.UUID `gorm:"type:char(36);primaryKey"`
    Code string    `gorm:"uniqueIndex;not null"`
}

// 在 Create 前生成 UUID
order.ID = uuid.New()
db.Create(&order)

[!tip] 为什么选 UUID 做主键? 自增 ID 存在 URL 泄露、ID 猜测等安全风险。UUID v4 完全随机,不会暴露数据量级。代价是索引碎片化更严重——InnoDB 的聚簇索引以主键排序,随机插入会导致页分裂,吞吐量下降约 30%。如果性能敏感可考虑 ULID(有序且随机),或雪花算法生成的 int64。

[!abstract] 子文档

  • 02-模型定义/UUID主键策略 — UUID 版本选型(v4/v7)、BeforeCreate 自动生成、存储优化(char(36) vs binary(16))、InnoDB 页分裂原理、ULID / Snowflake 替代方案、决策流程

复合主键

type OrderItem struct {
    OrderID   uint `gorm:"primaryKey"`   // 多个 primaryKey tag 构成复合主键
    ProductID uint `gorm:"primaryKey"`
    Quantity  int  `gorm:"not null"`
}
// 生成表结构:PRIMARY KEY (order_id, product_id)

[!warning] 复合主键的注意事项

  • First() 和 Take() 无法工作(它们期望单主键),必须用 Where("order_id=? AND product_id=?", ...) 精确定位。
  • AutoMigrate 对复合主键支持有限,建议手动建表或在迁移后验证 schema。
  • 关联查询时外键也需要对应多列——通常此时改用单独的 id 作为主键 + 唯一索引代替复合主键更为方便。

软删除

GORM 的软删除通过 DeletedAt 字段实现——被"删除"的记录实际上只是将该字段设为当前时间,数据仍然保留在数据库中。

type User struct {
    gorm.Model // DeletedAt 已包含在此中
}

// "删除"一条记录(实际是 UPDATE SET deleted_at = NOW())
db.Delete(&user)          // sql: UPDATE users SET deleted_at=... WHERE id=?

// 查询时自动排除已删除记录
db.Find(&users)           // WHERE deleted_at IS NULL

// 查询全部(包括已删除)
db.Unscoped().Find(&users)

// 真正物理删除
db.Unscoped().Delete(&user)

[!important] 软删除的设计哲学 软删除不是银弹。适合用于「可能需要恢复」的业务场景(如组织架构、订单流程)。但对于有唯一性约束的字段,软删除后会导致冲突——因为旧记录的该行仍占着唯一值。此时需自行设计隔离策略,例如添加 is_deleted INT DEFAULT 0 替代 GORM 内置方案。

[!question] 为什么软删除用时间戳而非 BOOLEAN? 时间戳记录了确切的删除时刻,便于后续审计和问题排查;而 BOOLEAN 只能告诉你「是否删除」,丢失了时序信息。同时,NULL 表示未删除,语义更加清晰。

自定义类型作为字段

当内置类型不够用时,GORM 提供三组接口让 struct 自行控制与数据库的交互方式。

ValueScanner 模式(推荐)

实现 driver.Valuer(写回 DB)和 sql.Scanner(从 DB 读出)两个接口:

type JSONMap map[string]interface{}

// Value:Go → DB(序列化)
func (j JSONMap) Value() (driver.Value, error) {
    if j == nil {
        return nil, nil
    }
    return json.Marshal(j) // 存入时转为 JSON 字符串
}

// Scan:DB → Go(反序列化)
func (j *JSONMap) Scan(value interface{}) error {
    if value == nil {
        *j = nil
        return nil
    }
    return json.Unmarshal(value.([]byte), &j)
}

type Setting struct {
    ID    uint      `gorm:"primaryKey"`
    Key   string    `gorm:"size:64;uniqueIndex;not null"`
    Value JSONMap   `gorm:"type:json"` // 存入 MySQL JSON 列
}

GORMDataType 覆盖类型推导

如果你只需要告诉 GORM「这个类型对应什么数据库类型」,无需处理序列化逻辑:

type Priority int8 // 优先级:1-紧急 2-高 3-普通 4-低

func (Priority) GORMDataType() string {
    return "TINYINT UNSIGNED" // 覆盖 GORM 默认的 INT 推导
}

type Task struct {
    ID       uint     `gorm:"primaryKey"`
    Title    string   `gorm:"size:128;not null"`
    Priority Priority `gorm:"not null;default:3"`
}

[!tip] 该用哪个接口?

  • 需要 序列化成特定格式(如 JSON、压缩字符串)→ 实现 driver.Valuer + sql.Scanner
  • 只需 覆盖列类型,不做特殊转换 → 实现 GORMDataType()
  • 两者可同时实现——GORMDataType 决定建表类型,Value/Scan 决定读写时的数据转换。

数据校验与生命周期钩子

struct tag 适合声明式约束,但更复杂的业务逻辑需要 GORM 提供的生命周期回调:

type User struct {
    ID        uint      `gorm:"primaryKey"`
    Email     string    `gorm:"size:128;uniqueIndex;not null"`
    Password  string    `gorm:"size:128;not null"`
    IsActive  bool      `gorm:"default:true"`
    CreatedAt time.Time
}

// BeforeCreate:在 INSERT 之前执行(可用于加密密码、生成唯一码等)
func (u *User) BeforeCreate(tx *gorm.DB) error {
    // 基础校验
    if !strings.Contains(u.Email, "@") {
        return errors.New("invalid email format") // 返回错误中断操作
    }
    return nil
}

// AfterFind:在 SELECT 之后执行(可用于解密敏感字段)
func (u *User) AfterFind(tx *gorm.DB) error {
    // 例如:对查询结果做脱敏处理
    return nil
}

[!summary] 常用钩子一览

钩子 触发时机 典型用途
BeforeCreate Create 之前 字段默认值、加密、校验
AfterCreate Create 之后 发送通知、记录日志
BeforeUpdate Update 之前 乐观锁版本检查
AfterUpdate Update 之后 同步缓存、审计追踪
BeforeDelete Delete 之前 关联级联清理
AfterDelete Delete 之后 软删除相关辅助操作
AfterFind Find/First 之后 字段解密、格式化输出

[!note] 校验的最佳实践 struct tag 的 not null / uniqueIndex 仅在 建表 时生效。运行时 GORM 不会自动校验这些数据——如果需要应用层校验,优先使用独立的 validator 库(如 github.com/go-playground/validator),而不是依赖生命周期钩子做业务校验。

模型设计流程图

graph TD
    A[定义 struct] --> B{有无 ID<br/>字段?}
    B -->|有| C[ID 自动作主键]
    B -->|无| D{实现 TableName?}
    D -->|已实现| E[使用返回值<br/>作为表名]
    D -->|未实现| F[struct 名 →<br/>蛇形复数形式]
    C --> G{需要自定义列名或<br/>类型?}
    E --> G
    F --> G
    G -->|是| H[添加 gorm tag]
    G -->|否| I{是否需要软删除?}
    H --> I
    I -->|是| J[嵌入 gorm.Model<br/>或使用 DeletedAt]
    I -->|否| K{内置类型够用?}
    J --> K
    K -->|是| L[完成 ✓]
    K -->|否| M[实现 Value / Scan<br/>接口]
    M --> L
    style A fill:#4FC08D,color:#fff
    style C fill:#3B82F6,color:#fff
    style J fill:#F59E0B,color:#fff
    style L fill:#A0AEC0,color:#fff

关联笔记