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/03-CRUD 操作.md
T
2026-05-17 00:06:11 +08:00

18 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
CRUD
增删改查
First
Find
Create
Update
Delete
Select
Omit
Count
Distinct
Raw SQL
Or
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] Omit vs Select 的选择策略

  • 要查询的列很少(例如 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

关联笔记