vault backup: 2026-05-06 13:42:02

This commit is contained in:
2026-05-06 13:42:02 +08:00
parent 00d939fa8b
commit b0a02e3b35
4 changed files with 504 additions and 8 deletions
+148 -6
View File
@@ -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()<br/>指定数据模型"] --> 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)