Files
cs-note/hhs/GORM/00-基本语法与 API 概览.md
T
2026-05-24 11:42:38 +08:00

553 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [GORM, Go, ORM, API, 链式调用, Session, Clauses, Context, Scopes]
create time: 2026-05-06 00:00
---
# 基本语法与 API 概览
## 概述
本章是 GORM 框架的「速查手册」——梳理 `*gorm.DB` 提供的核心方法、链式调用模式和常见用法。理解这些后,面对任何复杂场景你都能像搭积木一样组合出正确的 SQL。
> [!tip] 核心原则
> **GORM 的方法都是「不可变」的**:每次调用 `Where()`、`Select()`、`Clauses()` 等返回一个新的 `*gorm.DB` 实例,原始实例不变。这意味着你可以安全地复用同一个 DB 对象构建不同的查询。
```go
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) | 否 |
| 关联查询支持 | ✅ 可以 | ❌ 需要手动处理 |
```go
// 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` 解析表名:
```go
// 假设配置了前缀 + 单数模式
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()` 排除无关字段:
```go
// 将订单 user_id 改为 5(只改这一个字段,不触发钩子)
db.Model(&Order{}).Where("id = ?", orderID).Omit("updated_at").Update("user_id", 5)
```
---
### `db.Session()` — 创建带配置的独立会话
`Session` 允许你在单次操作中覆盖默认行为(跳过钩子、关闭事务等),不会影响全局配置。
```go
// 创建一个跳过所有 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 但不想真正执行?
> ```go
> var result User
> err := db.Session(&gorm.Session{DryRun: true}).First(&result, 1).Error
> // result 不会被填充,但生成的 SQL 会打到日志里(前提 Logger 没关掉)
> ```
## 高级控制 API
### `Clauses()` — SQL 级别的控制
当你需要用到数据库特有的功能(行锁、索引提示、表选项)时,`Clauses` 提供了结构化的方式,而不是直接写原生 SQL。
```go
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` 中防止未被前端传递的字段被零值覆盖:
```go
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 中**最容易被忽视但最有价值**的模式之一。它将一组查询条件封装成函数,像中间件一样在不同业务场景中复用:
```go
// 定义一个 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 对象 |
>
> ```go
> // 带参数的 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`。
```go
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`,适用于只需读取一行中部分列或执行聚合的场景:
```go
// 聚合查询:获取活跃用户数
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。
>
> ```go
> var avgAge float64
> db.Table("users").Select("AVG(age)").Row().Scan(&avgAge) // AVG 只能用 Row
> ```
### `Rows()` —— 多行游标
返回 `*sql.Rows`,需要手动管理 `Close()`:
```go
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-事务管理]]。
```go
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 的零值陷阱
> ```go
> // 用户年龄改为 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
按场景给出最常用的一行写法模板:
```go
// ─── 分页查询 ───
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 基础语法决策流程图
```mermaid
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策略/预编译语句