18 KiB
tags, create time
| tags | 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三个看起来很相似的方法?它们在什么场景下该用哪个?带着这个问题往下看。
五大查询方法
flowchart LR
Start["db.Model()<br/>指定数据模型"] --> Choice{选择方法}
Choice --> |单条| Single[First / Take / Last]
Choice --> |批量| Batch[Find]
Single --> SingleRet["返回 *User<br/>单条指针"]
Batch --> BatchRet["返回 []User<br/>切片"]
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是无序的(不指定排序),拿到的是任意一条。
// 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的结果会一样吗?答案:是的。当数据只有一条时,无论有无排序,结果都是它。区别在于多行数据时的行为。
实际用法示例
// 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 可以把查询结果直接映射到你自定义的结构体上,节省内存和带宽。
// 只查用户名和邮箱,扫描到轻量结构体
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 直接返回切片,无需额外处理。
// 获取所有用户名
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)
Or —— 条件"或"组合
Where 默认是"与"关系。当需要多个条件只要满足其一即可时,使用链式 Or:
// 查找名字为 "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 |
// 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] 为什么要用参数绑定而不是字符串拼接?
// ❌ 危险 — 字符串拼接可被 SQL 注入攻击 db.Raw("SELECT * FROM users WHERE name = '" + input + "'") // ✅ 安全 — 参数化查询由驱动层转义 db.Raw("SELECT * FROM users WHERE name = ?", input)
Rows —— 游标式逐行处理
对于超大数据集,一次性加载所有结果到内存会导致 OOM。此时可以用 Rows() 返回标准库的 *sql.Rows,逐行迭代:
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 — 指定要查哪些列
// 查 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)不需要频繁读取的场景:
// 查除了 content 和 password 之外的所有字段
db.Model(&Article{}).Omit("content", "password").Find(&articles)
// SELECT id, title, author_id, created_at FROM articles;
[!tip]
OmitvsSelect的选择策略
- 要查询的列很少(例如 3/20)→
Select更简洁,明确列出所需字段- 要排除的列很少(例如 3 个不要 / 17 个保留)→
Omit更省事- 原则:谁的参数少就用谁,代码更直观
- 敏感字段(密码、密钥等)永远用
Omit排除——白名单式防护,防止重构或新增字段时意外暴露
Distinct — 去重查询
// 获取所有不重复的角色
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 — 计数查询
// 统计总行数
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)
[!tip] 理解 GORM 的插入逻辑
db.Create(&user)背后执行的是标准的INSERT INTO ...SQL。GORM 会自动把结构体字段映射到数据库列 —— 前提是字段上配有gorm:"column:name"tag,或者遵循驼峰转下划线的命名约定。
// 单条插入
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
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 |
详细示例
// 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」。
批量更新
// 批量更新满足条件的行
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 语句,而是执行:
UPDATE users SET deleted_at = NOW() WHERE id = ?;
同时,后续所有查询(Find、First 等)自动附加 WHERE deleted_at IS NULL 条件,被「删除」的记录对常规查询不可见。
[!question] 软删除有什么优缺点?
| 优点 | 缺点 |
|---|---|
| 可恢复误删数据 | 占用存储空间,表越来越大 |
| 保留审计追踪(谁在什么时候删的) | 查询需要额外过滤,性能下降 |
| 避免外键约束问题 | 需要手动处理已删除记录的关联数据 |
// 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:db.Unscoped().Delete(&user, 10) // 真正的 DELETE FROM users WHERE id=10
[!warning] 执行前三思 物理删除是不可逆操作。建议在业务层先做「逻辑标记」(如加
status='deleted'字段),保留至少 30 天的恢复窗口。
错误处理
[!tip] GORM 的错误处理哲学 GORM 从不 panic,所有错误都通过返回值或
.Error字段返回。这意味着你可以在业务层优雅地处理异常,而不是让程序崩溃。
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
// ❌ 危险写法 —— 错误被静默吞掉了 db.Create(&user) // ✅ 正确写法 if err := db.Create(&user).Error; err != nil { log.Printf("创建失败: %v", err) }
CRUD 决策流程图
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