--- tags: [GORM, Go, ORM, CRUD, 增删改查, First, Find, Create, Update, Delete] create time: 2026-04-28 00:00 --- # CRUD 操作 ## 概述 CRUD(Create / Read / Update / Delete)是最基础的数据库操作。GORM 将 SQL 操作封装成了链式调用的 Go API,让你可以用「写 Go 代码」的方式与数据库交互。 想象一个电商场景:用户浏览商品 → 加入购物车 → 下单支付 → 查看订单历史 —— 这一连串动作背后就是 CRUD 在支撑。掌握 GORM 的查询、创建、更新和删除方法,就能覆盖日常开发中 **80% 的数据交互需求**。 > [!question] 读到这里你想过吗? > 为什么 GORM 要设计 `First`、`Take`、`Last` 三个看起来很相似的方法?它们在什么场景下该用哪个?带着这个问题往下看。 ## 五大查询方法 ```mermaid flowchart LR Start[db.Model(&User{})] --> Choice{选择方法} Choice --> |单条| Single[First / Take / Last] Choice --> |批量| Batch[Find] Single --> SingleRet["返回 *User
单条指针"] Batch --> BatchRet["返回 []User
切片"] style Start fill:#4FC08D,color:#fff style Choice fill:#3B82F6,color:#fff style SingleRet fill:#10B981,color:#fff style BatchRet fill:#F59E0B,color:#fff ``` ### 方法对比 | 方法 | 返回类型 | 行为 | 是否需要 Where | |------|----------|------|----------------| | `First` | 单条指针 | 按主键排序取第一条,**必须有条件** | ✅ 必须 | | `Take` | 单条指针 | 随机取一条(ORDER BY 随机),**必须有条件** | ✅ 必须 | | `Last` | 单条指针 | 按主键倒序取最后一条 | ✅ 必须 | | `Find` | 切片 | 查询所有匹配行 | ❌ 可选 | | `Get` | 单条指针 | `First` 的封装,自动处理空结果 | 同 First | ### First vs Take 的区别 > [!tip] 核心差异一句话总结 > `First` 是**有序的**(按主键升序),结果可预期;`Take` 是**无序的**(不指定排序),拿到的是任意一条。 ```go // First: 按主键升序取第一条(确定性结果) db.First(&user) // SELECT * FROM users ORDER BY primary_key ASC LIMIT 1; WHERE ? // 每次执行,只要数据没变,拿到的永远是同一条记录 // Take: 不按任何顺序,数据库返回哪条就是哪条 db.Take(&user) // SELECT * FROM users LIMIT 1; // 适合随机抽奖、每日推荐等场景 —— 你不在乎是哪条,只在乎「随机」这个动作 ``` > [!question] 思考题 > 如果表中只有一条记录,`First` 和 `Take` 的结果会一样吗? > > > **答案**:是的。当数据只有一条时,无论有无排序,结果都是它。区别在于多行数据时的行为。 ### 实际用法示例 ```go // 1. First - 根据主键或条件查找单条记录 var user User db.First(&user, 10) // WHERE id = 10 —— 按主键查询 db.First(&user, "name = ?", "john") // WHERE name = 'john' —— 自定义条件 // 2. Take - 随机取一条(抽奖、推荐场景常用) var product Product db.Take(&product) // 从全表中随机拿一条商品 // 3. Find - 批量查询返回切片 var users []User db.Where("age > ?", 18).Find(&users) // WHERE age > 18 —— 成年用户列表 // 4. Get - 实际上是 First + 错误检查的组合写法 err := db.First(&user).Error if errors.Is(err, gorm.ErrRecordNotFound) { // 记录不存在 —— 需要给用户返回友好提示 } ``` **执行流程图解**:`First` → GORM 自动生成 `ORDER BY 主键 ASC LIMIT 1 WHERE ...` → 返回指针。如果找不到记录,会在 `.Error` 中设置 `gorm.ErrRecordNotFound` 而不是直接抛 panic。 > [!danger] 常见误区 > `Find` 不带 `Where` 会查询全表!数据量大时会导致 OOM。始终记得加过滤条件,或用 `Limit` 兜底。 ## 补充查询方法 除了前面提到的五大核心方法,GORM 还有两个非常实用的查询 API: ### Scan — 将结果扫描到任意结构体 当你不需要完整模型,只想提取部分字段时,`Scan` 可以把查询结果直接映射到你自定义的结构体上,节省内存和带宽。 ```go // 只查用户名和邮箱,扫描到轻量结构体 type UserSummary struct { Name string `gorm:"column:name"` Email string `gorm:"column:email"` } var summaries []UserSummary db.Table("users").Select("name, email").Scan(&summaries) ``` > [!tip] 使用场景 > - **报表统计**:只需几个聚合字段时,避免加载整个模型 > - **API 响应**:防止泄露敏感字段(如密码、内部标记) ### Pluck — 提取单一列 想快速获取某一列的所有值?`Pluck` 直接返回切片,无需额外处理。 ```go // 获取所有用户名 var names []string db.Model(&User{}).Pluck("name", &names) // SELECT name FROM users; -> []string{"Alice", "Bob", ...} // 配合 Distinct 去重 var roles []string db.Model(&User{}).Pluck("DISTINCT role", &roles) ``` > [!question] 思考题 > `Scan` 和 `Find` 有什么区别?什么时候该用哪个? > > > **答案**:`Find` 返回模型切片,适合后续还要调用 GORM 方法;`Scan` 可以映射到任意结构体甚至非结构体变量,更灵活但失去了 GORM 的类型安全保护。 ## 创建(Create) > [!tip] 理解 GORM 的插入逻辑 > `db.Create(&user)` 背后执行的是标准的 `INSERT INTO ...` SQL。GORM 会自动把结构体字段映射到数据库列 —— 前提是字段上配有 `gorm:"column:name"` tag,或者遵循驼峰转下划线的命名约定。 ```go // 单条插入 user := User{Name: "Alice", Age: 30} result := db.Create(&user) // result.RowsAffected — 影响行数(通常返回 1) // result.Error — 错误信息(如唯一约束冲突时会报错) // 批量插入(传入 slice,GORM 自动生成 VALUES (...), (...), (...)) users := []User{ {Name: "Bob", Age: 25}, {Name: "Charlie", Age: 35}, } db.Create(&users) // INSERT INTO users (name, age) VALUES ('Bob', 25), ('Charlie', 35) ``` > [!question] 为什么 GORM 要自动回填 ID? > > 因为在 RESTful API 中,你经常需要在创建资源后立即返回新资源的 URL 或 ID。GORM 在 INSERT 完成后会立即查询最后插入的 ID 并赋值给结构体,省去了二次查询的麻烦。 > [!tip] 批量插入上限 > GORM 内部会将批量 insert 拆分为每批约 256 条,避免单次 SQL 过大。如需调整,可通过 `db.Session(&gorm.Session{FullSaveRecords: true})` 控制行为。 ### 插入后获取自增 ID ```go db.Create(&user) fmt.Println(user.ID) // 插入后 ID 自动回填到结构体 ``` ## 更新(Update) > [!tip] 理解三种更新策略的差异 > 在实际业务中,你可能只想改一个字段、更新部分字段、或者全量覆盖。GORM 为此提供了不同粒度的 API —— 选错了会导致「该更新的没更新」或「不该更新的被覆盖」。 ### 方法速查表 | 方法 | 签名 | 行为 | |------|------|------| | `Update` | `(col string, value any)` | 更新单个字段 | | `Updates` | `(value Model | map[string]any)` | 更新一个或多个字段 | | `Save` | `(value Model)` | 全量更新所有字段 | | `UpdateColumn` | `(col string, value any)` | 同 Update 但不触发 Hooks | | `UpdateColumns` | `(value Model)` | 同 Updates 但不触发 Hooks | ### 详细示例 ```go // 1. Update - 单字段更新(最简洁的改法) db.Model(&user).Update("name", "Bob") // UPDATE users SET name='Bob', updated_at=... WHERE id=... // ⚡ 只生成一个 SET 子句,SQL 最小化 // 2. Updates - 多字段(map 方式:零值也会被写入数据库) db.Model(&user).Updates(map[string]any{ "name": "Alice", "age": 0, // ✅ age 会被更新为 0 "role": "admin", }) // 适合前端传来的完整表单数据 —— 你希望任何字段都原样写入 // 3. Updates - 多字段(struct 方式:跳过零值字段!) db.Model(&user).Updates(User{Name: "Alice", Role: "admin"}) // 只有 Name 和 Role 被更新,Age、Status 等未填写的字段保持不变 // ⚡ GORM 会自动比对哪些字段「非零」才生成 SET 语句 // 4. Save - 全量覆盖(无视零值,全部字段写回数据库) db.Save(&user) // UPDATE users SET name='Alice', age=0, status='', ... WHERE id=... // 谨慎使用 —— 会把所有字段重新写一遍,包括可能被意外覆盖的敏感字段 ``` > [!warning] Zero Values 陷阱 —— struct vs map 的关键区别 | 传入方式 | 零值处理 | 典型场景 | |----------|---------|---------| | `Updates(struct)` | 跳过零值字段 | 局部修改(如只改昵称) | | `Updates(map)` | 正常写入零值 | 前端完整表单提交 | > **真实案例**:用户把年龄从 `25` 改为 `0`,如果用 struct 方式调用 `Updates(User{Age: 0})`,Age 根本不会出现在 SQL 中,导致数据没有被更新!这就是为什么上表里说「需要根据场景选 API」。 ### 批量更新 ```go // 批量更新满足条件的行 db.Model(&User{}).Where("status = ?", "active").Update("role", "vip") // UPDATE users SET role='vip' WHERE status='active'; ``` ## 删除(Delete) > [!tip] GORM 的删除不是「删了就完事」 > 理解软删除和物理删除的区别,是安全操作数据库的第一步。在生产环境中,**默认倾向软删除**是更稳妥的做法。 ### 软删除原理 当模型包含 `DeletedAt` 字段时,GORM 会自动开启软删除功能。此时调用 `db.Delete(&user)` **不会执行 DELETE 语句**,而是执行: ```sql UPDATE users SET deleted_at = NOW() WHERE id = ?; ``` 同时,后续所有查询(Find、First 等)**自动附加** `WHERE deleted_at IS NULL` 条件,被「删除」的记录对常规查询不可见。 > [!question] 软删除有什么优缺点? | 优点 | 缺点 | |------|------| | 可恢复误删数据 | 占用存储空间,表越来越大 | | 保留审计追踪(谁在什么时候删的) | 查询需要额外过滤,性能下降 | | 避免外键约束问题 | 需要手动处理已删除记录的关联数据 | ```go // 1. 根据主键物理删除 db.Delete(&user, 10) // DELETE FROM users WHERE id = 10 —— 不可恢复! // 2. 条件批量删除(删除所有未成年用户) db.Where("age < ?", 18).Delete(&User{}) // DELETE FROM users WHERE age < 18; // 3. 软删除(模型含 DeletedAt 时生效) db.Delete(&user) // UPDATE users SET deleted_at=NOW() WHERE id=? // 数据还在库里,只是查询时自动被过滤掉了 ``` > [!info] 物理删除 — 彻底擦除 > 当你确认需要永久移除数据时(如合规要求),使用 `Unscoped`: > ```go > db.Unscoped().Delete(&user, 10) // 真正的 DELETE FROM users WHERE id=10 > ``` > [!warning] 执行前三思 > 物理删除是不可逆操作。建议在业务层先做「逻辑标记」(如加 `status='deleted'` 字段),保留至少 30 天的恢复窗口。 ## 错误处理 > [!tip] GORM 的错误处理哲学 > GORM **从不 panic**,所有错误都通过返回值或 `.Error` 字段返回。这意味着你可以在业务层优雅地处理异常,而不是让程序崩溃。 ```go result := db.First(&user, 100) if errors.Is(result.Error, gorm.ErrRecordNotFound) { // 用户不存在 —— 可以返回 404 或默认值 fmt.Println("用户不存在") } else if result.Error != nil { // 数据库层面的错误(连接失败、SQL 语法错误等) fmt.Println("数据库错误:", result.Error) } ``` | 错误常量 | 含义 | 典型处理策略 | |----------|------|-------------| | `gorm.ErrRecordNotFound` | 查询结果为空 | 返回 404 或默认值 | | `gorm.ErrDuplicatedKey` | 唯一约束冲突 | 提示用户「用户名已存在」 | | `gorm.ErrInvalidData` | 插入/更新的数据无效 | 校验前端输入后重试 | | `gorm.ErrTxAlreadyCommitted` | 事务已提交 | 检查事务逻辑是否有重复调用 | > [!warning] 永远不要忽略 Error > ```go > // ❌ 危险写法 —— 错误被静默吞掉了 > db.Create(&user) > > // ✅ 正确写法 > if err := db.Create(&user).Error; err != nil { > log.Printf("创建失败: %v", err) > } > ``` ## CRUD 决策流程图 ```mermaid flowchart TD Start[收到数据操作请求] --> Type{操作类型_} Type --> |读取| ReadQ{要几条_} ReadQ --> |1条| OneQ{需要确定性顺序_} OneQ --> |是| FirstNode[First_按主键升序] OneQ --> |否_随机| TakeNode[Take_随机一条] ReadQ --> |N条| FindNode[Find_加Where条件] Type --> |新增| CreateQ{单条还是批量_} CreateQ --> |单条| CreateSingle[Create] CreateQ --> |批量| CreateBatch[Create_slice] Type --> |修改| UpdateQ{改多少字段_} UpdateQ --> |1个| UpdateOne[Update_col_val] UpdateQ --> |部分| ZeroQ{需要更新零值_} ZeroQ --> |是| UpdateMap[Updates_map] ZeroQ --> |否| UpdateStruct[Updates_struct] UpdateQ --> |全部| SaveNode[Save] Type --> |删除| SoftQ{启用SoftDelete_} SoftQ --> |是| SoftDel[Delete_软删除] SoftQ --> |否| HardDel[Delete_物理删除] style Start fill:#4FC08D,color:#fff style FirstNode fill:#3B82F6,color:#fff style CreateSingle fill:#8B5CF6,color:#fff style SaveNode fill:#EC4899,color:#fff style SoftDel fill:#F59E0B,color:#000 ``` ## 关联笔记 - [[01-安装与初始化]] - [[02-模型定义]] - [[04-条件查询]] - [[10-软删除]] - [[14-错误处理]]