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/00-基本语法与 API 概览.md
T
2026-05-17 00:06:11 +08:00

20 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
API
链式调用
Session
Clauses
Context
Scopes
2026-05-06 00:00

基本语法与 API 概览

概述

本章是 GORM 框架的「速查手册」——梳理 *gorm.DB 提供的核心方法、链式调用模式和常见用法。理解这些后,面对任何复杂场景你都能像搭积木一样组合出正确的 SQL。

[!tip] 核心原则 GORM 的方法都是「不可变」的:每次调用 Where()、Select()、Clauses() 等返回一个新的 *gorm.DB 实例,原始实例不变。这意味着你可以安全地复用同一个 DB 对象构建不同的查询。

db := GetDB() // 假设已初始化的全局 DB 实例

// ✅ 安全:三个独立查询互不影响
q1 := db.Where("age > ?", 18)   // 新实例 A
q2 := db.Where("role = ?", "admin") // 新实例 B
db.Find(&users)                 // 回到原始实例,不带任何 Where —— ⚠️ 注意不是 q1 也不是 q2

[!question] 为什么这样设计? 因为 *gorm.DB 内部存储了连接池、配置和当前构建的 Clauses。如果直接修改原实例,并发请求会互相污染——每个 goroutine 拿到的查询会被其他 goroutine 打断。返回新实例保证了线程安全和可组合性。

指定操作目标

db.Model() vs db.Table()

这是最常用的两个 API,它们告诉 GORM「你要操作什么」。

特性 Model(&User{}) Table("orders")
传入类型 struct / struct 指针 string 表名
是否读取 model 信息 是(字段映射、主键、软删除) 否(纯字符串表名)
适用场景 结构化操作 视图、临时表、跨库
自动加表名前缀 是(根据 NamingStrategy) 否
关联查询支持 ✅ 可以 ❌ 需要手动处理
// Model — 推荐日常使用
db.Model(&User{}).Find(&users)           // SELECT * FROM users
db.Model(&user).Updates(User{Name: "x"}) // UPDATE users SET name='x' WHERE id=?

// Table — 灵活但丢失 model 信息
db.Table("user_view").Select("name, email").Find(&results)
db.Table("users").Where("deleted_at IS NULL").Count(&count)

Model() — 命名策略与前缀自动处理

当使用 Model(&User{}) 时,GORM 会按配置中的 NamingStrategy 解析表名:

// 假设配置了前缀 + 单数模式
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
    NamingStrategy: schema.NamingStrategy{
        TablePrefix:   "crm_",
        SingularTable: true,
    },
})

db.Model(&User{}).Find(&users)
// → SELECT * FROM crm_user  ✅ 自动加前缀 + 变单数

db.Table("users").Find(&users)
// → SELECT * FROM users     ❌ 完全忽略命名策略

[!warning] 常见陷阱:Table 绕过命名策略 团队协作中最容易犯的错误——有人为了"灵活"大量使用 db.Table("xxx"),后来改了 TablePrefix,结果查询查到了裸表(没有前缀)。排查这种问题非常耗时。 建议:项目中统一使用 Model(),需要跨库时才退回到 Table("other_db.table_name")。

Model() — 级联更新技巧

当需要更新某个关联记录的所属关系时,用 Model() 指定主模型、Omit() 排除无关字段:

// 将订单 user_id 改为 5(只改这一个字段,不触发钩子)
db.Model(&Order{}).Where("id = ?", orderID).Omit("updated_at").Update("user_id", 5)

db.Session() — 创建带配置的独立会话

Session 允许你在单次操作中覆盖默认行为(跳过钩子、关闭事务等),不会影响全局配置。

// 创建一个跳过所有 Hook 的会话
sess := db.Session(&gorm.Session{
    SkipHooks: true,                    // 跳过所有钩子函数
    Logger: logger.Default.LogMode(logger.Silent), // 本次查询不输出日志
    DryRun: true,                        // 仅生成 SQL 不执行(调试模式)
})

sql, params := sess.Model(&User{}).Where("id = ?", 1).ToSQL(
    func(tx *gorm.DB) *gorm.DB { return tx.Find(&User{}) },
)
fmt.Println(sql, params) // 打印实际执行的 SQL 和参数

[!tip] DryRun + ToSQL 实战 在迁移脚本或测试中,想看生成的 SQL 但不想真正执行?

var result User
err := db.Session(&gorm.Session{DryRun: true}).First(&result, 1).Error
// result 不会被填充,但生成的 SQL 会打到日志里(前提 Logger 没关掉)

高级控制 API

Clauses() — SQL 级别的控制

当你需要用到数据库特有的功能(行锁、索引提示、表选项)时,Clauses 提供了结构化的方式,而不是直接写原生 SQL。

import "gorm.io/gorm/clause"

// 1. FOR UPDATE — 行锁(事务内防止并发修改)
db.Clauses(clause.Locking{
    Strength: "UPDATE", // SELECT ... FOR UPDATE
    Tables:   []clause.Table{{Name: "orders"}},
}).Where("status = ?", "pending").Find(&orders)

// 2. ON CONFLICT — MySQL 的 INSERT IGNORE / UPSERT
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{Name: "email"},
    DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// INSERT INTO users (email, name, age) VALUES (...)
// ON DUPLICATE KEY UPDATE name=VALUES(name), age=VALUES(age)

// 3. INDEX HINT — 指导优化器走特定索引
db.Clauses(
    clause.Select{},                      // 选择哪些列
    clause.From{Joins: []clause.Join{     // JOIN 条件
        {
            Table: clause.Table{Name: "profiles"},
            On: &clause.Expr{
                Vars: []any{"users.id = profiles.user_id"},
            },
        },
    }},
).Find(&users)

// 4. ORDER BY RAND() — 随机排序(MySQL 专用)
db.Clauses(clause.OrderByColumn{
    Column: clause.Column{Name: "RAND()"},
}).Find(&users)

[!warning] ON CONFLICT 的版本兼容性

  • MySQL 8.0.19+:支持 ON DUPLICATE KEY UPDATE,通过 OnConflict 生成
  • MySQL < 8.0:改用 DoNothing: true 生成 ON DUPLICATE KEY UPDATE id=id(效果等同于忽略冲突行)
  • PostgreSQL:直接用 OnConflict,语法一致

Assign() — 创建时赋关联值

在 Create 一个记录的同时,给它设置关联的外键或 Belongs-to 关系;或者在 Updates 中防止未被前端传递的字段被零值覆盖:

type Article struct {
    ID     uint
    Title  string
    AuthorID uint `gorm:"index"`
}

// 场景一:前端只传来文章标题和作者 ID,直接创建
authorID := uint(42)
db.Model(&Article{}).Assign(Article{AuthorID: authorID}).
    Create(&Article{Title: "Hello World"})
// INSERT INTO articles (author_id, title) VALUES (42, 'Hello World')

// 场景二:Updates 不会自动忽略未传字段 → 用 Assign 精确控制
var req struct {
    Status string
}
req.Status = "published" // 只传了 status,Price 等字段未传
db.Model(&Product{}).Where("id = ?", id).
    Assign(Product{Status: req.Status}).
    Updates(req)
// 只更新 status,不会因为 Product.Price == 0 而把价格清零

[!example] Assign 适合的场景

  • 前端只传了一个 ID,你需要用它设置外键关系时(不用先查再写)
  • Updates 防零值污染:结构体中有未传来的字段,默认零值会覆盖数据库数据,用 Assign 只写入你指定的值
  • 无主数据批量创建:没有完整 Model,只有部分字段的临时数据

Scopes() —— 可复用的查询条件链

Scopes 是 GORM 中最容易被忽视但最有价值的模式之一。它将一组查询条件封装成函数,像中间件一样在不同业务场景中复用:

// 定义一个 Scope 函数——参数和返回值都是 *gorm.DB
func Active(db *gorm.DB) *gorm.DB {
    return db.Where("status = ?", "active")
}

func CreatedAfter(t time.Time) *gorm.DB {
    return db.Where("created_at > ?", t)
}

// 使用 —— 自由组合多个 Scope
var users []User
db.Scopes(Active, CreatedAfter(time.Now().AddDate(0, 0, -30))).
    Order("created_at DESC").
    Find(&users)
// WHERE status = 'active' AND created_at > '...' ORDER BY created_at DESC

[!tip] Scopes vs 中间态 DB(变量保存 Where)

维度 Scopes(函数) 中间态 DB(变量)
复用粒度 跨模块、跨文件 同一请求内的不同分支
可组合性 Scopes(f1, f2) 自由组合 需要手动拼接
参数化 支持闭包传入参数 直接在变量上追加
测试友好 每个 Scope 独立可测 需构造完整 DB 对象
// 带参数的 Scope —— 通过闭包传参
func Page(page, pageSize int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}

// 使用
db.Scopes(Page(2, 20)).Find(&items)

[!tip] Scope 编写规范

  • 参数始终是 *gorm.DB,返回值也是 *gorm.DB——这是 GORM 约定的签名
  • 不要修改传入的 DB 的配置(如 Logger),只追加查询条件
  • Scope 可以组合任意数量的 Where / Order / Select / Omit 等操作
  • 需要 SQL 注入防护时(如动态排序字段),在 Scope 内部做白名单校验

WithContext() / Ctx — 上下文集成

生产环境中经常需要超时控制、取消请求、传递 trace ID。GORM 完全兼容 Go 标准库的 context.Context。

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 方式一:With 链式调用
err := db.WithContext(ctx).First(&user, 10).Error

// 方式二:直接传入 WithContext 后的 DB
tx := db.WithContext(ctx)
tx.Create(&order)
tx.Update(...)
// 整个链共享同一个超时时间

[!important] 什么时候必须用 WithContext?

场景 是否必需
HTTP 请求超时控制 ✅ 强烈推荐
批量操作长时间运行 ✅ 防止阻塞
单元测试 mock 超时 ✅ 隔离测试
本地快速原型 ❌ 可省略

原生接口对接

GORM 虽然是 ORM,但在某些场景下你必须退回到标准库的 *sql 接口。

Row() —— 单行原生查询

返回 *sql.Row,适用于只需读取一行中部分列或执行聚合的场景:

// 聚合查询:获取活跃用户数
var count int64
db.Table("users").Where("status = ?", "active").
    Select("COUNT(*)").Row().Scan(&count)

// 多字段提取:从一个结果行里取指定列
var id uint
var email string
db.Table("users").Where("id = ?", 42).
    Select("id", "email").Row().Scan(&id, &email)

[!question] Row().Scan 和 Count() / First() 怎么选?

  • Count 场景 → 用 Count(&count),GORM 自动帮你拼 SELECT COUNT(*),语义最清晰。
  • 单个 Model 对象 → 用 First(&user, id) 或 Take(&user),结构体自动填充。
  • 混合字段 / 聚合函数(AVG、MAX、GROUP BY)→ 此时 Find / First 不好使,用 Row() 手动 Scan。
var avgAge float64
db.Table("users").Select("AVG(age)").Row().Scan(&avgAge) // AVG 只能用 Row

Rows() —— 多行游标

返回 *sql.Rows,需要手动管理 Close():

rows, err := db.Table("events").Order("time ASC").Rows()
if err != nil { /* handle */ }
defer rows.Close() // ⚠️ 忘记 Close 会导致连接泄漏!

for rows.Next() {
    var id uint
    var eventTime time.Time
    var eventType string
    rows.Scan(&id, &eventTime, &eventType)
    consumeEvent(id, eventTime, eventType)
}

if err := rows.Err(); err != nil {
    log.Printf("rows iteration error: %v", err)
}

事务入口

GORM 的事务通过 Begin() 开启,返回 *gorm.DB,之后所有的操作都在这个事务上下文中执行。详见 08-事务管理。

tx := db.Begin()         // 开始事务
defer tx.Rollback()      // 异常时回滚

user := User{Name: "test", Age: 25}
if err := tx.Create(&user).Error; err != nil {
    return err           // 出错就 rollback
}

return tx.Commit().Error // 全部成功才提交

API 速查参考卡

面向快速决策——面对一个具体需求时,不知道该用哪个 API?参考下面的对照表。

读取单条记录

方法 SQL 行为 是否必须有条件 适用场景
First(&v, cond) ORDER BY PK ASC LIMIT 1 + WHERE ✅ 必须 按主键或条件取确定的一条
Take(&v) 无排序,随机一条 ✅ 必须 抽奖、随机推荐
Last(&v, cond) ORDER BY PK DESC LIMIT 1 ✅ 必须 取最新的一条

经验法则:拿不到记录时会返回 gorm.ErrRecordNotFound。永远检查 .Error。详见 03-CRUD 操作。

批量查询

方法 返回类型 是否需要 Where 说明
Find(&slice) []Model ❌ 可选 默认查所有匹配行,不加 Where = 全表扫描
Pluck("col", &slice) 切片(非 Model) ❌ 可选 只取一列的值,如 []string{"Alice","Bob"}
Scan(&dest) 任意结构体 ❌ 可选 把结果映射到自定义结构体

更新策略对比

方法 参数类型 零值处理 触发 Hooks 典型场景
Update(col, val) string + any — ✅ 改单个字段
Updates(map) map[string]any 写入零值 ✅ 前端完整表单提交
Updates(struct) struct 跳过零值 ✅ 部分字段修改
Save struct 全部写入 ✅ 全量覆盖(谨慎使用)
UpdateColumn(...) 同 Update — ❌ 跳过 绕过钩子直接写

[!warning] Updates 的零值陷阱

// 用户年龄改为 0 —— 如果用 struct,Age=0 被跳过,数据库不变!
db.Model(&user).Updates(User{Name: "Alice", Age: 0})  // ⚠️ Age 没变

// ✅ 改用 map
db.Model(&user).Updates(map[string]any{"name": "Alice", "age": 0})  // ✅ Age 变为 0

详见 03-CRUD 操作。

创建方式对比

方法 适用场景 批次控制 说明
Create(&model) 单条插入 无 最基础的插入
Create(&slice) 中小批量(≤10K) GORM 自动拆批 ~256 内部拆分为多条 INSERT
CreateInBatches(&slice, n) 超大批量(>10K) 可指定 batch size 适合数据导入、迁移
Clauses(OnConflict)...Create() Upsert 同上 存在则更新,不存在则插入

详见 11-批量操作。

字段选择

方法 行为 示例
Select("col1, col2") 白名单:只读这些列 db.Select("id,name").Find(&users)
Omit("col1", "col2") 黑名单:不读这些列 db.Omit("password","bio").Find(&users)
Pluck("col", &dest) 只取一列的值 db.Pluck("email", &emails)

选择策略:需要的列少 → Omit;需要的列多 → Select。敏感字段永远建议 Omit。

删除方式

方法 行为 是否可恢复
Delete(&model) 软删除(有 DeletedAt 时)→ UPDATE ✅ 可恢复
Where(...).Delete(&Model{}) 条件批量删除 取决于模型是否有软删除
Unscoped().Delete(...) 物理删除(忽略软删除) ❌ 不可恢复

详见 10-软删除。


常见链式组合 Recipes

按场景给出最常用的一行写法模板:

// ─── 分页查询 ───
page, size := 2, 20
offset := (page - 1) * size
var items []Item
db.Model(&Item{}).
    Where("status = ?", "active").
    Order("created_at DESC").
    Limit(size).
    Offset(offset).
    Find(&items)

// ─── 乐观锁 ───
type Versioned struct {
    ID      uint `gorm:"primaryKey"`
    Version int
    Payload string
}
db.Model(&Versioned{}).
    Where("id = ? AND version = ?", id, oldVersion).
    Updates(map[string]any{"payload": "new", "version": oldVersion + 1})

// ─── UPSERT(存在则更新,不存在则插入) ───
db.Clauses(clause.OnConflict{
    Columns:   []clause.Column{Name: "uid"},
    DoUpdates: clause.AssignmentColumns([]string{"name", "email"}),
}).Create(&user)

// ─── 批量赋值更新 ───
ids := []uint{1, 2, 3}
db.Model(&Article{}).Where("id IN ?", ids).Updates(Article{Status: "published"})

// ─── 带 JOIN 的查询(无关联定义时的手动做法) ───
type ArticleWithAuthor struct {
    gorm.Model
    Title    string
    AuthorName string `gorm:"column:author_name"`
}
db.Table("articles a").
    Select("a.*, u.name as author_name").
    Joins("JOIN users u ON a.author_id = u.id").
    Where("a.status = ?", "published").
    Find(&articles)

// ─── 预编译语句(同一 SQL 多次执行) ───
db.PrepareStmt(true).Find(&users) // 返回新 DB 实例,启用 Prepared Stmt 缓存

// ─── 手动预处理(单次会话内复用) ───
sess := db.Session(&gorm.Session{PrepareStmt: true})
for _, id := range ids {
    sess.First(&user, id) // 首次执行会缓存 Prepared Stmt
}

GORM 基础语法决策流程图

flowchart TD
    Start["收到数据操作需求"] --> Target{指定操作目标}
    
    Target -->|"有 struct 模型"| Model["db.Model(&User{})"]
    Target -->|"视图/临时表/跨库"| Table["db.Table('xxx')"]
    
    Model --> Control{"需要特殊控制?"}
    Table --> Control
    
    Control -->|"行锁/UPSERT/索引提示"| Clauses["Clauses(clause.xxx)"]
    Control -->|"跳过钩子/调试"| Session["&gorm.Session{SkipHooks/DryRun}"]
    Control -->|"关联赋值"| Assign["Assign(值).Create/Update"]
    Control -->|"超时/取消"| Context["WithContext(ctx)"]
    Control -->|"条件需复用"| Scopes["Scopes(f1, f2)"]
    Control -->|"普通查询继续链式调用"| Chain["正常链式调用 Where/Order/Limit"]
    
    Chain --> MethodQ{"读取几条?"}
    
    MethodQ --> |1条| SingleQ{需要确定性?}
    SingleQ --> |是| First["First - ORDER BY PK ASC LIMIT 1"]
    SingleQ --> |否_随机| Take["Take - 随机一条"]
    
    MethodQ --> |N条| FindMode{"查哪些列?"}
    FindMode --> |全列| Find["Find - 加 Where 过滤"]
    FindMode --> |单列| Pluck["Pluck - 只取一列"]
    FindMode --> |部分列| SelectOmit{"少选多?"}
    SelectOmit --> |选少| Omit["Omit - 排除法"]
    SelectOmit --> |选多| Select["Select - 白名单"]
    
    MethodQ --> |超大结果集| Rows["Rows() 游标逐行消费"]
    
    Chain --> UpdateQ{"改几个字段?"}
    
    UpdateQ --> |1个| OneField["Update(col, val)"]
    UpdateQ --> |部分| ZeroQ{含零值?}
    ZeroQ --> |是_map_| MapUpdate["Updates(map)"]
    ZeroQ --> |否_struct_| StructUpdate["Updates(struct)"]
    UpdateQ --> |全部| FullUpdate["Save - 全量覆盖"]
    
    UpdateQ --> |无钩子需求| NoHook["UpdateColumn / UpdateColumns"]
    
    Clauses --> Result["执行 CRUD 动作"]
    Session --> Result
    Assign --> Result
    Context --> Result
    Scopes --> Result
    First --> Result
    Take --> Result
    Find --> Result
    Pluck --> Result
    Omit --> Result
    Select --> Result
    Rows --> Result
    OneField --> Result
    MapUpdate --> Result
    StructUpdate --> Result
    FullUpdate --> Result
    NoHook --> Result
    
    style Start fill:#4FC08D,color:#fff
    style Model fill:#3B82F6,color:#fff
    style Table fill:#EAB308,color:#000
    style Scopes fill:#8B5CF6,color:#fff
    style Result fill:#10B981,color:#fff

关联笔记