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-06 13:42:02 +08:00

354 lines
12 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]
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-事务管理]]