354 lines
12 KiB
Markdown
354 lines
12 KiB
Markdown
|
|
---
|
|||
|
|
tags: [GORM, Go, ORM, API, 链式调用, Session, Clauses, Context]
|
|||
|
|
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)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] `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,只有部分字段的临时数据
|
|||
|
|
|
|||
|
|
### `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 // 全部成功才提交
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 常见链式组合 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 -->|"普通查询继续链式调用"| Chain["正常链式调用 Where/Order/Limit"]
|
|||
|
|
|
|||
|
|
Chain --> Result["Find/First/Create/Update/Delete"]
|
|||
|
|
Clauses --> Result
|
|||
|
|
Session --> Result
|
|||
|
|
Assign --> Result
|
|||
|
|
Context --> Result
|
|||
|
|
|
|||
|
|
style Start fill:#4FC08D,color:#fff
|
|||
|
|
style Model fill:#3B82F6,color:#fff
|
|||
|
|
style Table fill:#EAB308,color:#000
|
|||
|
|
style Result fill:#10B981,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[01-安装与初始化]]
|
|||
|
|
- [[02-模型定义]]
|
|||
|
|
- [[03-CRUD 操作]]
|
|||
|
|
- [[04-条件查询]]
|
|||
|
|
- [[08-事务管理]]
|