diff --git a/hhs/GORM/00-基本语法与 API 概览.md b/hhs/GORM/00-基本语法与 API 概览.md new file mode 100644 index 0000000..dc27287 --- /dev/null +++ b/hhs/GORM/00-基本语法与 API 概览.md @@ -0,0 +1,353 @@ +--- +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-事务管理]] diff --git a/hhs/GORM/02-模型定义/自定义类型作为字段.md b/hhs/GORM/02-模型定义/自定义类型作为字段.md index 591390e..bd845c4 100644 --- a/hhs/GORM/02-模型定义/自定义类型作为字段.md +++ b/hhs/GORM/02-模型定义/自定义类型作为字段.md @@ -11,8 +11,8 @@ create time: 2026-05-06 14:30 ```mermaid flowchart LR - GoVal["Go 结构体值"] -->|driver.Valuer.Value() → 序列化→| DBVal["数据库列值"] - DBVal -->|sql.Scanner.Scan() ← 反序列化←| GoVal + GoVal["Go 结构体值"] -->|"driver.Valuer.Value() → 序列化 →"| DBVal["数据库列值"] + DBVal -->|"sql.Scanner.Scan() ← 反序列化 ←"| GoVal style GoVal fill:#3B82F6,color:#fff style DBVal fill:#EAB308,color:#fff diff --git a/hhs/GORM/03-CRUD 操作.md b/hhs/GORM/03-CRUD 操作.md index 9d222c6..78cfbad 100644 --- a/hhs/GORM/03-CRUD 操作.md +++ b/hhs/GORM/03-CRUD 操作.md @@ -1,5 +1,5 @@ --- -tags: [GORM, Go, ORM, CRUD, 增删改查, First, Find, Create, Update, Delete] +tags: [GORM, Go, ORM, CRUD, 增删改查, First, Find, Create, Update, Delete, Select, Omit, Count, Distinct, Raw SQL, Or] create time: 2026-04-28 00:00 --- @@ -18,7 +18,7 @@ CRUD(Create / Read / Update / Delete)是最基础的数据库操作。GORM ```mermaid flowchart LR - Start[db.Model(&User{})] --> Choice{选择方法} + Start["db.Model()
指定数据模型"] --> Choice{选择方法} Choice --> |单条| Single[First / Take / Last] Choice --> |批量| Batch[Find] @@ -127,10 +127,152 @@ var roles []string db.Model(&User{}).Pluck("DISTINCT role", &roles) ``` -> [!question] 思考题 -> `Scan` 和 `Find` 有什么区别?什么时候该用哪个? -> -> > **答案**:`Find` 返回模型切片,适合后续还要调用 GORM 方法;`Scan` 可以映射到任意结构体甚至非结构体变量,更灵活但失去了 GORM 的类型安全保护。 +### Or —— 条件"或"组合 + +`Where` 默认是"与"关系。当需要多个条件**只要满足其一即可**时,使用链式 `Or`: + +```go +// 查找名字为 "Alice" 或者角色为 "admin" 的用户 +db.Where("name = ?", "Alice").Or("role = ?", "admin").Find(&users) +// SELECT * FROM users WHERE name = 'Alice' OR role = 'admin'; + +// 多组"或"嵌套 —— 用括号包裹 +db.Where("role = ?", "admin"). + Where(db.Or("status = ?", "active").Or("status = ?", "super")). + Find(&users) +// SELECT * FROM users WHERE role = 'admin' AND (status = 'active' OR status = 'super'); +``` + +> [!warning] Or 和 Where 的顺序很重要 +> GORM 将 `Or` 附加到**前一个 Where 所在的逻辑组**。先写 `Where + Or` 再套 `Where`,得到的是 `A OR B AND C` 而非 `(A OR B) AND C`。如果你期望后面的括号效果,请用 `db.Or()` 显式包裹。 + +### Raw SQL —— 原生 SQL 查询与执行 + +有些场景 GORM 无法优雅表达(如复杂子查询、存储过程调用),这时可以退回到原生 SQL。GORM 提供了两类 API: + +| 方法 | 用途 | 返回 | +|------|------|------| +| `Raw(sql, args...)` | 构造原生查询,仍可链式调 Select/Where 等 | `*gorm.DB` | +| `Exec(sql, args...)` | 直接执行 DML/DDL,不返回数据行 | `sql.Result` | + +```go +// 1. Raw — 原生 SQL + GORM 链式方法混搭 +var results []User +db.Raw("SELECT * FROM users WHERE created_at > ?", time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)). + Order("id DESC"). + Find(&results) +// 用原生 SQL 写初始过滤条件,再用 GORM 链做排序和映射 + +// 2. Exec — 执行无返回结果的语句(INSERT/UPDATE/DELETE/DDL) +db.Exec("DELETE FROM expired_sessions") +db.Exec("ALTER TABLE orders ADD COLUMN remark TEXT") + +// 3. 带参数绑定的原生查询(防 SQL 注入) +var count int +db.Raw("SELECT COUNT(*) FROM users WHERE status = ?", "active").Scan(&count) +fmt.Println("活跃用户数:", count) +``` + +> [!tip] 为什么要用参数绑定而不是字符串拼接? +> ```go +> // ❌ 危险 — 字符串拼接可被 SQL 注入攻击 +> db.Raw("SELECT * FROM users WHERE name = '" + input + "'") +> +> // ✅ 安全 — 参数化查询由驱动层转义 +> db.Raw("SELECT * FROM users WHERE name = ?", input) +> ``` + +### Rows —— 游标式逐行处理 + +对于超大数据集,一次性加载所有结果到内存会导致 OOM。此时可以用 `Rows()` 返回标准库的 `*sql.Rows`,逐行迭代: + +```go +rows, err := db.Table("logs").Where("level = ?", "error").Rows() +if err != nil { ... } +defer rows.Close() + +for rows.Next() { + var id uint + var msg string + rows.Scan(&id, &msg) // 手动扫描字段 + processError(id, msg) // 自定义处理逻辑 +} +``` + +> [!question] 什么时候该用 Rows 而不是 Find? +> - **数据量极大**(百万级以上)→ `Rows()` 逐行消费,内存可控 +> - **每行处理成本高** → 避免全部加载后再逐个处理 +> - **普通场景(几千条以内)** → `Find()` 更简洁,不用手动管理 `Close()` + +## 字段选择查询 + +掌握了如何查数据后,接下来思考一个问题:**每次全量读取整张表真的是最高效的方式吗?** + +显然不是——只需要的字段才去读,既能节省网络带宽,也能减少序列化的开销。 + +### Select — 指定要查哪些列 + +```go +// 查 name 和 email 两个字段,扫描到完整模型(仅加载部分字段) +type UserLite struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:64"` + Email string `gorm:"size:128"` +} + +var users []UserLite +db.Model(&User{}).Select("id, name, email").Find(&users) +// SELECT id, name, email FROM users; + +// 结合 Pluck 提取单一列 +var emails []string +db.Model(&User{}).Pluck("email", &emails) +// SELECT email FROM users; -> []string{"alice@example.com", ...} +``` + +### Omit — 排除某些列 + +与 `Select` 相反,`Omit` 告诉 GORM **不要读取**这些字段,适合大文本字段(如 `content`、`bio`)不需要频繁读取的场景: + +```go +// 查除了 content 和 password 之外的所有字段 +db.Model(&Article{}).Omit("content", "password").Find(&articles) +// SELECT id, title, author_id, created_at FROM articles; +``` + +> [!tip] `Omit` vs `Select` 的选择策略 +> - 需要的列少(比如 3 个字段中有 20 个)→ `Omit` 更省事 +> - 需要的列多 → `Select` 更清晰 +> - 敏感字段(密码、内部密钥)永远建议 `Omit`,以防误暴露 + +### Distinct — 去重查询 + +```go +// 获取所有不重复的角色 +var roles []string +db.Model(&User{}).Distinct("role").Pluck("role", &roles) +// SELECT DISTINCT role FROM users; -> ["admin", "user", "editor"] + +// 组合使用 — 去重 + 排序 +db.Model(&Order{}).Distinct().Order("created_at DESC").Find(&orders) +// SELECT DISTINCT * FROM orders ORDER BY created_at DESC; +``` + +### Count — 计数查询 + +```go +// 统计总行数 +var total int64 +db.Model(&User{}).Count(&total) +// SELECT COUNT(*) FROM users; -> total = 1523 + +// 配合条件统计 +db.Model(&User{}).Where("status = ?", "active").Count(&total) +// SELECT COUNT(*) FROM users WHERE status = 'active'; +``` + +> [!tip] Count 为什么返回值是 `int64`? +> MySQL 的 `COUNT()` 函数在结果超过 2^31 时可能超出 `int32` 范围,Go 中 `int` 在 32 位系统是 32 位,所以 GORM 统一返回 `int64` 保证安全。 ## 创建(Create) diff --git a/hhs/GORM/README.md b/hhs/GORM/README.md index 5c3cdae..f84d7b1 100644 --- a/hhs/GORM/README.md +++ b/hhs/GORM/README.md @@ -13,6 +13,7 @@ GORM 是 Go 生态中最流行的 ORM 库,本知识库系统整理 GORM 的核 ### 1. 基础篇 +- **[00-基本语法与 API 概览](./00-基本语法与 API 概览)** — Model vs Table、链式不可变机制、Session/Clauses/Assign、Context 集成、Recipes - **[01-安装与初始化](./01-安装与初始化)** — 依赖安装、DB 连接配置、全局配置项 - **[02-模型定义](./02-模型定义)** — struct tag 映射、字段类型、主键策略、表名规则 - **[03-CRUD 操作](./03-CRUD 操作)** — Create / First / Take / Find / Get 五大方法