20 KiB
tags, create time
| tags | create time | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
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
关联笔记
- 01-安装与初始化
- 02-模型定义
- 03-CRUD 操作 — 详细版 First/Find/Create/Update/Delete
- 04-条件查询 — Where/Or/Between/In/Like 详解
- 05-关联查询 — Preload/Joins/Association
- 06-排序与分页 — Order/Limit/Paginate
- 08-事务管理 — Begin/Commit/Rollback
- 09-钩子函数 — LifecycleHooks/全局Callback
- 10-软删除 — SoftDelete/Unscoped
- 11-批量操作 — Bulk Insert/Update/Upsert
- 14-错误处理 — RecordNotFound/ErrDuplicatedKey
- 15-性能优化 — N+1/Preload策略/预编译语句