--- 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策略/预编译语句