vault backup: 2026-04-28 20:12:15
This commit is contained in:
+207
-30
@@ -25,6 +25,9 @@ type User struct {
|
||||
// 这就是一个最简单的 GORM Model
|
||||
```
|
||||
|
||||
> [!tip] 核心原则
|
||||
> GORM 的本质是一个「约定优于配置」的 ORM。struct 只是普通的 Go 结构体,`gorm:"..."` tag 告诉 GORM 如何将它映射到数据库——不修改运行时行为,只在构建 SQL 和建表时生效。这意味着你可以将同一个 struct 同时用于 HTTP 请求体和数据库模型(通过暴露合适的字段)。
|
||||
|
||||
## 表名规则
|
||||
|
||||
### 默认命名策略
|
||||
@@ -35,14 +38,21 @@ GORM 使用 `Table` 方法来推导表名:
|
||||
|----------|------|
|
||||
| 默认 | struct 名的蛇形复数形式(`User` → `users`) |
|
||||
| 实现 `TableName() string` | 返回的字符串 |
|
||||
| 使用 `db.Table("xxx")` | 查询时使用的表名(不会修改 Model 的 TableName) |
|
||||
| 使用 `db.Table("xxx")` | 本次查询使用的表名(不会修改 Model 的 TableName) |
|
||||
|
||||
> [!tip] `TableName()` vs `db.Table()`:何时用哪个?
|
||||
> - **结构性差异**(多环境、分库分表)→ 用 `TableName()`,因为这是模型级别的约定。
|
||||
> - **临时覆盖**(一个复杂查询需要 JOIN 不同视图)→ 用 `db.Table()`,它只影响当前链式调用,不污染模型定义。
|
||||
|
||||
```go
|
||||
func (User) TableName() string {
|
||||
return "sys_user" // 显式指定表名
|
||||
return "sys_user" // 显式指定表名;value receiver 即可,无需接收者变量
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] 为什么用 value receiver `(User)` 而非 `*User`?
|
||||
> `TableName()` 只需要返回一个字符串,不需要访问任何字段。GORM 内部会先尝试值接收者方法,找不到再尝试指针接收者——所以用 `(User)` 既能被值对象调用也能被指针对象调用,灵活性更好。
|
||||
|
||||
> [!tip] 多环境表名
|
||||
> 不同环境下表名前缀不同?可以用环境变量 + `TableName()` 实现动态切换:
|
||||
> ```go
|
||||
@@ -74,7 +84,8 @@ gorm:"column:name;type:bigint;not null;default:0;uniqueIndex;index;comment:用
|
||||
| `index` | 普通索引 | `index:idx_status` |
|
||||
| `comment` | 注释(MySQL) | `comment:用户名` |
|
||||
| `<-` | 字段写入控制 | `<-:true` / `<-:false` / `<-:create` / `<-:update` |
|
||||
| `->` | 只读/只写 | `->:true`(只读) `/ <-:false`(不写入) |
|
||||
| `->` | 读取控制(只读 = 只从 DB 读出) | `->:true` / `->:false`(不读取回 struct) |
|
||||
| `<-` + `->` | 组合控制读写 | `<-:false;->:true`(只读,不可写入) |
|
||||
| `-` | 忽略此字段 | `-` |
|
||||
|
||||
### 写入权限控制
|
||||
@@ -93,6 +104,47 @@ type Product struct {
|
||||
> [!example] 方向记忆法
|
||||
> `<-` 表示数据**流向数据库**(写入),`->` 表示数据**从数据库流出**(读取)。箭头方向就是数据的方向。
|
||||
|
||||
### 嵌入 Struct(组合优于继承)
|
||||
|
||||
Go 不支持类的继承,但可以通过结构体嵌套实现代码复用:
|
||||
|
||||
```go
|
||||
// 方式一:显式嵌入公共字段
|
||||
type AuditRecord struct {
|
||||
CreatedBy string `gorm:"size:64"` // 创建人
|
||||
UpdatedBy string `gorm:"size:64"` // 最后修改人
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:64;not null"`
|
||||
AuditRecord // 匿名嵌入——Go 将 AuditRecord 的字段提升为 User 的直接字段
|
||||
}
|
||||
|
||||
// 方式二:使用 gorm.Model(推荐 🌟)
|
||||
type Article struct {
|
||||
gorm.Model // 自动包含 ID、CreatedAt、UpdatedAt、DeletedAt
|
||||
Title string `gorm:"size:128;not null"`
|
||||
Content string `gorm:"type:text"`
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] 为什么推荐 `gorm.Model`?
|
||||
> `gorm.Model` 是 GORM 预定义的 struct,包含四个最常见字段:
|
||||
> ```go
|
||||
> type Model struct {
|
||||
> ID any `gorm:"primaryKey"` // 主键,类型为 any (v2.27+)
|
||||
> CreatedAt time.Time // 创建时间
|
||||
> UpdatedAt time.Time // 更新时间
|
||||
> DeletedAt gorm.DeletedAt // 软删除时间戳
|
||||
> }
|
||||
> ```
|
||||
> 嵌入后即可拥有完整的审计追踪 + 软删除能力,无需每个 struct 重复声明。
|
||||
|
||||
> [!question] 思考
|
||||
> 如果 `User` 同时嵌入了 `AuditRecord` 和 `gorm.Model`,而两者都有 `CreatedBy` 字段,会发生什么?
|
||||
> **答**:编译报错——Go 不允许歧义字段访问。此时应只保留一个来源,或改为普通成员变量而非嵌入:`Audit AuditRecord`。
|
||||
|
||||
## 字段类型映射
|
||||
|
||||
| Go 类型 | 推荐 DB 类型 | 说明 |
|
||||
@@ -112,13 +164,15 @@ type Product struct {
|
||||
type Article struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Title string `gorm:"size:128;not null"`
|
||||
CreatedAt time.Time `gorm:"autoTime;createTime"` // 创建时自动填充
|
||||
UpdatedAt time.Time `gorm:"autoTime;updateTime"` // 更新时自动填充
|
||||
CreatedAt time.Time `gorm:"autoTime;createTime"` // 首次插入时自动填入当前时间,后续 UPDATE 不会修改
|
||||
UpdatedAt time.Time `gorm:"autoTime;updateTime"` // 每次 UPDATE 时自动更新为当前时间
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] createTime / updateTime
|
||||
> 这两个子 tag 是 GORM v2 的增强功能,配合 `autoTime` 使用。单独 `autoTime` 只更新时间,加上 `createTime` 后才会在插入时设置 CreatedAt。
|
||||
> [!tip] `autoTime` + `createTime` / `updateTime` 的分工
|
||||
> - 只写 `autoTime`(旧写法):GORM 在 INSERT 和 UPDATE 时都更新时间字段。
|
||||
> - `autoTime` + `createTime`:INSERT 时设置 `CreatedAt`,UPDATE 不碰它——这才是创建时间的正确语义。
|
||||
> - `autoTime` + `updateTime`:UPDATE 时设置 `UpdatedAt`,INSERT 时同样会设初始值。
|
||||
|
||||
## 主键策略
|
||||
|
||||
@@ -146,56 +200,179 @@ order.ID = uuid.New()
|
||||
db.Create(&order)
|
||||
```
|
||||
|
||||
> [!tip] 为什么选 UUID 做主键?
|
||||
> 自增 ID 存在 URL 泄露、ID 猜测等安全风险。UUID v4 完全随机,不会暴露数据量级。代价是索引碎片化更严重——InnoDB 的聚簇索引以主键排序,随机插入会导致页分裂,吞吐量下降约 30%。如果性能敏感可考虑 [ULID](https://github.com/ulid/spec)(有序且随机),或雪花算法生成的 `int64`。
|
||||
|
||||
### 复合主键
|
||||
|
||||
```go
|
||||
type OrderItem struct {
|
||||
OrderID uint `gorm:"primaryKey"`
|
||||
OrderID uint `gorm:"primaryKey"` // 多个 primaryKey tag 构成复合主键
|
||||
ProductID uint `gorm:"primaryKey"`
|
||||
Quantity int `gorm:"not null"`
|
||||
}
|
||||
// 生成表结构:PRIMARY KEY (order_id, product_id)
|
||||
```
|
||||
|
||||
> [!tip] 复合主键注意事项
|
||||
> 使用复合主键时,`First()` 和 `Take()` 将无法工作(因为它们期望单主键),必须使用 `Where()` 精确定位。
|
||||
> [!warning] 复合主键的注意事项
|
||||
> - `First()` 和 `Take()` 无法工作(它们期望单主键),必须用 `Where("order_id=? AND product_id=?", ...)` 精确定位。
|
||||
> - `AutoMigrate` 对复合主键支持有限,建议手动建表或在迁移后验证 schema。
|
||||
> - 关联查询时外键也需要对应多列——通常此时改用单独的 `id` 作为主键 + 唯一索引代替复合主键更为方便。
|
||||
|
||||
## 软删除
|
||||
|
||||
GORM 的软删除通过 `DeletedAt` 字段实现——被"删除"的记录实际上只是将该字段设为当前时间,数据仍然保留在数据库中。
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
gorm.Model // DeletedAt 已包含在此中
|
||||
}
|
||||
|
||||
// "删除"一条记录(实际是 UPDATE SET deleted_at = NOW())
|
||||
db.Delete(&user) // sql: UPDATE users SET deleted_at=... WHERE id=?
|
||||
|
||||
// 查询时自动排除已删除记录
|
||||
db.Find(&users) // WHERE deleted_at IS NULL
|
||||
|
||||
// 查询全部(包括已删除)
|
||||
db.Unscoped().Find(&users)
|
||||
|
||||
// 真正物理删除
|
||||
db.Unscoped().Delete(&user)
|
||||
```
|
||||
|
||||
> [!important] 软删除的设计哲学
|
||||
> 软删除不是银弹。适合用于「可能需要恢复」的业务场景(如组织架构、订单流程)。但对于有唯一性约束的字段,软删除后会导致冲突——因为旧记录的该行仍占着唯一值。此时需自行设计隔离策略,例如添加 `is_deleted INT DEFAULT 0` 替代 GORM 内置方案。
|
||||
|
||||
> [!question] 为什么软删除用时间戳而非 BOOLEAN?
|
||||
> 时间戳记录了确切的删除时刻,便于后续审计和问题排查;而 BOOLEAN 只能告诉你「是否删除」,丢失了时序信息。同时,`NULL` 表示未删除,语义更加清晰。
|
||||
|
||||
## 自定义类型作为字段
|
||||
|
||||
当内置类型不够用时,可以实现 GORM 接口:
|
||||
当内置类型不够用时,GORM 提供三组接口让 struct 自行控制与数据库的交互方式。
|
||||
|
||||
### ValueScanner 模式(推荐)
|
||||
|
||||
实现 `driver.Valuer`(写回 DB)和 `sql.Scanner`(从 DB 读出)两个接口:
|
||||
|
||||
```go
|
||||
type MyType int
|
||||
type JSONMap map[string]interface{}
|
||||
|
||||
func (m MyType) GORMDataType() string {
|
||||
return "INT"
|
||||
// Value:Go → DB(序列化)
|
||||
func (j JSONMap) Value() (driver.Value, error) {
|
||||
if j == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return json.Marshal(j) // 存入时转为 JSON 字符串
|
||||
}
|
||||
|
||||
func (m MyType) Value() (driver.Value, error) {
|
||||
return int(m), nil // ScannerValuer 接口的 Value 方法
|
||||
// Scan:DB → Go(反序列化)
|
||||
func (j *JSONMap) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*j = nil
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(value.([]byte), &j)
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Key string `gorm:"size:64;uniqueIndex;not null"`
|
||||
Value JSONMap `gorm:"type:json"` // 存入 MySQL JSON 列
|
||||
}
|
||||
```
|
||||
|
||||
### GORMDataType 覆盖类型推导
|
||||
|
||||
如果你只需要告诉 GORM「这个类型对应什么数据库类型」,无需处理序列化逻辑:
|
||||
|
||||
```go
|
||||
type Priority int8 // 优先级:1-紧急 2-高 3-普通 4-低
|
||||
|
||||
func (Priority) GORMDataType() string {
|
||||
return "TINYINT UNSIGNED" // 覆盖 GORM 默认的 INT 推导
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Title string `gorm:"size:128;not null"`
|
||||
Priority Priority `gorm:"not null;default:3"`
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 该用哪个接口?
|
||||
> - 需要 **序列化成特定格式**(如 JSON、压缩字符串)→ 实现 `driver.Valuer` + `sql.Scanner`
|
||||
> - 只需 **覆盖列类型**,不做特殊转换 → 实现 `GORMDataType()`
|
||||
> - 两者可同时实现——`GORMDataType` 决定建表类型,`Value/Scan` 决定读写时的数据转换。
|
||||
|
||||
## 数据校验与生命周期钩子
|
||||
|
||||
struct tag 适合声明式约束,但更复杂的业务逻辑需要 GORM 提供的生命周期回调:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Email string `gorm:"size:128;uniqueIndex;not null"`
|
||||
Password string `gorm:"size:128;not null"`
|
||||
IsActive bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// BeforeCreate:在 INSERT 之前执行(可用于加密密码、生成唯一码等)
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
// 基础校验
|
||||
if !strings.Contains(u.Email, "@") {
|
||||
return errors.New("invalid email format") // 返回错误中断操作
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterFind:在 SELECT 之后执行(可用于解密敏感字段)
|
||||
func (u *User) AfterFind(tx *gorm.DB) error {
|
||||
// 例如:对查询结果做脱敏处理
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
> [!summary] 常用钩子一览
|
||||
> | 钩子 | 触发时机 | 典型用途 |
|
||||
> |------|----------|----------|
|
||||
> | `BeforeCreate` | `Create` 之前 | 字段默认值、加密、校验 |
|
||||
> | `AfterCreate` | `Create` 之后 | 发送通知、记录日志 |
|
||||
> | `BeforeUpdate` | `Update` 之前 | 乐观锁版本检查 |
|
||||
> | `AfterUpdate` | `Update` 之后 | 同步缓存、审计追踪 |
|
||||
> | `BeforeDelete` | `Delete` 之前 | 关联级联清理 |
|
||||
> | `AfterDelete` | `Delete` 之后 | 软删除相关辅助操作 |
|
||||
> | `AfterFind` | `Find/First` 之后 | 字段解密、格式化输出 |
|
||||
|
||||
> [!note] 校验的最佳实践
|
||||
> struct tag 的 `not null` / `uniqueIndex` 仅在 **建表** 时生效。运行时 GORM 不会自动校验这些数据——如果需要应用层校验,优先使用独立的 validator 库(如 `github.com/go-playground/validator`),而不是依赖生命周期钩子做业务校验。
|
||||
|
||||
## 模型设计流程图
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[定义 struct] --> B{有无 ID 字段?}
|
||||
B -->|有| C["ID 作主键<br/>uint/int64"]
|
||||
B -->|无| D{自定义 TableName?}
|
||||
D -->|实现了| E[按 TableName 返回值]
|
||||
D -->|未实现| F["struct 名蛇形复数"]
|
||||
C --> G[添加字段 tag]
|
||||
graph TD
|
||||
A[定义 struct] --> B{有无 ID<br/>字段?}
|
||||
B -->|有| C[ID 自动作主键]
|
||||
B -->|无| D{实现 TableName?}
|
||||
D -->|已实现| E[使用返回值<br/>作为表名]
|
||||
D -->|未实现| F[struct 名 →<br/>蛇形复数形式]
|
||||
C --> G{需要自定义列名或<br/>类型?}
|
||||
E --> G
|
||||
G --> H{需要特殊类型?}
|
||||
H -->|是| I[实现 GORMDataType / Valuer]
|
||||
H -->|否| J[完成]
|
||||
I --> J
|
||||
|
||||
F --> G
|
||||
G -->|是| H[添加 gorm tag]
|
||||
G -->|否| I{是否需要软删除?}
|
||||
H --> I
|
||||
I -->|是| J[嵌入 gorm.Model<br/>或使用 DeletedAt]
|
||||
I -->|否| K{内置类型够用?}
|
||||
J --> K
|
||||
K -->|是| L[完成 ✓]
|
||||
K -->|否| M[实现 Value / Scan<br/>接口]
|
||||
M --> L
|
||||
style A fill:#4FC08D,color:#fff
|
||||
style C fill:#3B82F6,color:#fff
|
||||
style I fill:#EAB308,color:#fff
|
||||
style J fill:#A0AEC0,color:#fff
|
||||
style J fill:#F59E0B,color:#fff
|
||||
style L fill:#A0AEC0,color:#fff
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
+198
-84
@@ -7,25 +7,28 @@ create time: 2026-04-28 00:00
|
||||
|
||||
## 概述
|
||||
|
||||
CRUD 是最基础的数据库操作。GORM 提供了语义清晰的五个核心查询方法(First / Take / Find / Get / Last)和多种更新删除 API。掌握它们就能覆盖日常 80% 的数据交互需求。
|
||||
CRUD(Create / Read / Update / Delete)是最基础的数据库操作。GORM 将 SQL 操作封装成了链式调用的 Go API,让你可以用「写 Go 代码」的方式与数据库交互。
|
||||
|
||||
想象一个电商场景:用户浏览商品 → 加入购物车 → 下单支付 → 查看订单历史 —— 这一连串动作背后就是 CRUD 在支撑。掌握 GORM 的查询、创建、更新和删除方法,就能覆盖日常开发中 **80% 的数据交互需求**。
|
||||
|
||||
> [!question] 读到这里你想过吗?
|
||||
> 为什么 GORM 要设计 `First`、`Take`、`Last` 三个看起来很相似的方法?它们在什么场景下该用哪个?带着这个问题往下看。
|
||||
|
||||
## 五大查询方法
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[db.Model(&User{})] --> B{First / Take / Last}
|
||||
A --> C[Find - 批量查询]
|
||||
A --> D[Get - 封装版 First]
|
||||
B --> E["单条记录<br/>返回 *User"]
|
||||
C --> F["多条记录切片<br/>返回 []User"]
|
||||
D --> E
|
||||
Start[db.Model(&User{})] --> Choice{选择方法}
|
||||
Choice --> |单条| Single[First / Take / Last]
|
||||
Choice --> |批量| Batch[Find]
|
||||
|
||||
style A fill:#4FC08D,color:#fff
|
||||
style B fill:#3B82F6,color:#fff
|
||||
style C fill:#8B5CF6,color:#fff
|
||||
style E fill:#10B981,color:#fff
|
||||
style F fill:#F59E0B,color:#fff
|
||||
style D fill:#EC4899,color:#fff
|
||||
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
|
||||
```
|
||||
|
||||
### 方法对比
|
||||
@@ -40,58 +43,120 @@ flowchart LR
|
||||
|
||||
### First vs Take 的区别
|
||||
|
||||
```go
|
||||
// First: 按主键升序取第一条(确定性)
|
||||
db.First(&user) // SELECT * FROM users ORDER BY primary_key LIMIT 1; WHERE ?
|
||||
> [!tip] 核心差异一句话总结
|
||||
> `First` 是**有序的**(按主键升序),结果可预期;`Take` 是**无序的**(不指定排序),拿到的是任意一条。
|
||||
|
||||
// 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 - 根据主键查找
|
||||
// 1. First - 根据主键或条件查找单条记录
|
||||
var user User
|
||||
db.First(&user, 10) // WHERE id = 10
|
||||
db.First(&user, "name = ?", "john") // WHERE name = 'john'
|
||||
db.First(&user, 10) // WHERE id = 10 —— 按主键查询
|
||||
db.First(&user, "name = ?", "john") // WHERE name = 'john' —— 自定义条件
|
||||
|
||||
// 2. Take - 随机取一条(抽奖场景常用)
|
||||
// 2. Take - 随机取一条(抽奖、推荐场景常用)
|
||||
var product Product
|
||||
db.Take(&product) // 任意一条商品
|
||||
db.Take(&product) // 从全表中随机拿一条商品
|
||||
|
||||
// 3. Find - 批量查询
|
||||
// 3. Find - 批量查询返回切片
|
||||
var users []User
|
||||
db.Where("age > ?", 18).Find(&users) // WHERE age > 18
|
||||
db.Where("age > ?", 18).Find(&users) // WHERE age > 18 —— 成年用户列表
|
||||
|
||||
// 4. Get - 安全版的 First
|
||||
// 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 — 影响行数
|
||||
// result.Error — 错误
|
||||
// result.RowsAffected — 影响行数(通常返回 1)
|
||||
// result.Error — 错误信息(如唯一约束冲突时会报错)
|
||||
|
||||
// 批量插入(至少两条才生效批量优化)
|
||||
// 批量插入(传入 slice,GORM 自动生成 VALUES (...), (...), (...))
|
||||
users := []User{
|
||||
{Name: "Bob", Age: 25},
|
||||
{Name: "Charlie", Age: 35},
|
||||
}
|
||||
db.Create(&users) // INSERT INTO users ... VALUES (...), (...), (...)
|
||||
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})` 控制行为。
|
||||
|
||||
@@ -104,7 +169,8 @@ fmt.Println(user.ID) // 插入后 ID 自动回填到结构体
|
||||
|
||||
## 更新(Update)
|
||||
|
||||
GORM 提供多种更新粒度,从单个字段到全量替换:
|
||||
> [!tip] 理解三种更新策略的差异
|
||||
> 在实际业务中,你可能只想改一个字段、更新部分字段、或者全量覆盖。GORM 为此提供了不同粒度的 API —— 选错了会导致「该更新的没更新」或「不该更新的被覆盖」。
|
||||
|
||||
### 方法速查表
|
||||
|
||||
@@ -119,31 +185,38 @@ GORM 提供多种更新粒度,从单个字段到全量替换:
|
||||
### 详细示例
|
||||
|
||||
```go
|
||||
// 1. Update - 单字段
|
||||
// 1. Update - 单字段更新(最简洁的改法)
|
||||
db.Model(&user).Update("name", "Bob")
|
||||
// UPDATE users SET name='Bob', updated_at=... WHERE id=...
|
||||
// ⚡ 只生成一个 SET 子句,SQL 最小化
|
||||
|
||||
// 2. Updates - 多字段(map 方式,零值也会被更新)
|
||||
// 2. Updates - 多字段(map 方式:零值也会被写入数据库)
|
||||
db.Model(&user).Updates(map[string]any{
|
||||
"name": "Alice",
|
||||
"age": 0, // 注意:0 也会被写入
|
||||
"age": 0, // ✅ age 会被更新为 0
|
||||
"role": "admin",
|
||||
})
|
||||
// 适合前端传来的完整表单数据 —— 你希望任何字段都原样写入
|
||||
|
||||
// 3. Updates - 多字段(struct 方式,只更新非零值字段!)
|
||||
// 3. Updates - 多字段(struct 方式:跳过零值字段!)
|
||||
db.Model(&user).Updates(User{Name: "Alice", Role: "admin"})
|
||||
// 只有 Name 和 Role 被更新,Age 保持不变
|
||||
// 只有 Name 和 Role 被更新,Age、Status 等未填写的字段保持不变
|
||||
// ⚡ GORM 会自动比对哪些字段「非零」才生成 SET 语句
|
||||
|
||||
// 4. Save - 全量覆盖(全部字段都写回去)
|
||||
// 4. Save - 全量覆盖(无视零值,全部字段写回数据库)
|
||||
db.Save(&user)
|
||||
// UPDATE users SET name='Alice', age=30, role='admin', ... WHERE id=...
|
||||
// UPDATE users SET name='Alice', age=0, status='', ... WHERE id=...
|
||||
// 谨慎使用 —— 会把所有字段重新写一遍,包括可能被意外覆盖的敏感字段
|
||||
```
|
||||
|
||||
> [!warning] Zero Values 陷阱
|
||||
> `Updates` 用 struct 传入时,**零值字段("" 0 false)不会被更新**。如果需要更新零值,改用 `map[string]any` 方式:
|
||||
> ```go
|
||||
> db.Model(&user).Updates(map[string]any{"age": 0}) // ✅ 能更新为 0
|
||||
> ```
|
||||
> [!warning] Zero Values 陷阱 —— struct vs map 的关键区别
|
||||
|
||||
| 传入方式 | 零值处理 | 典型场景 |
|
||||
|----------|---------|---------|
|
||||
| `Updates(struct)` | 跳过零值字段 | 局部修改(如只改昵称) |
|
||||
| `Updates(map)` | 正常写入零值 | 前端完整表单提交 |
|
||||
|
||||
> **真实案例**:用户把年龄从 `25` 改为 `0`,如果用 struct 方式调用 `Updates(User{Age: 0})`,Age 根本不会出现在 SQL 中,导致数据没有被更新!这就是为什么上表里说「需要根据场景选 API」。
|
||||
|
||||
### 批量更新
|
||||
|
||||
@@ -155,74 +228,115 @@ db.Model(&User{}).Where("status = ?", "active").Update("role", "vip")
|
||||
|
||||
## 删除(Delete)
|
||||
|
||||
```go
|
||||
// 根据主键删除
|
||||
db.Delete(&user, 10) // DELETE FROM users WHERE id = 10
|
||||
> [!tip] GORM 的删除不是「删了就完事」
|
||||
> 理解软删除和物理删除的区别,是安全操作数据库的第一步。在生产环境中,**默认倾向软删除**是更稳妥的做法。
|
||||
|
||||
// 批量删除
|
||||
db.Where("age < ?", 18).Delete(&User{})
|
||||
### 软删除原理
|
||||
|
||||
// 软删除(如果模型包含 DeletedAt)
|
||||
db.Delete(&user) // 不是 DELETE,而是 UPDATE ... SET deleted_at=NOW()
|
||||
当模型包含 `DeletedAt` 字段时,GORM 会自动开启软删除功能。此时调用 `db.Delete(&user)` **不会执行 DELETE 语句**,而是执行:
|
||||
|
||||
```sql
|
||||
UPDATE users SET deleted_at = NOW() WHERE id = ?;
|
||||
```
|
||||
|
||||
> [!info] 软删除 vs 物理删除
|
||||
> 物理 `DELETE FROM` 不可逆且丢失审计痕迹。如果你启用了 `SoftDelete`(模型含 `DeletedAt` 字段),默认执行软删除。
|
||||
>
|
||||
> 想强制执行物理删除?使用 `Unscoped`:
|
||||
同时,后续所有查询(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
|
||||
> 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` | 查询结果为空 |
|
||||
| `gorm.ErrInvalidData` | 插入/更新的数据无效 |
|
||||
| `gorm.ErrTxAlreadyCommitted` | 事务已提交 |
|
||||
| `gorm.ErrSavedValueNotNull` | 保存了非空字段的零值 |
|
||||
| 错误常量 | 含义 | 典型处理策略 |
|
||||
|----------|------|-------------|
|
||||
| `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
|
||||
A[收到数据操作请求] --> B{操作类型?}
|
||||
B -->|读取| C{要几条?}
|
||||
C -->|1 条| D{需要确定性顺序?}
|
||||
D -->|是| E[First - 按主键升序]
|
||||
D -->|否/随机| F[Take - 随机一条]
|
||||
C -->|N 条| G[Find + Where 条件]
|
||||
Start[收到数据操作请求] --> Type{操作类型_}
|
||||
|
||||
B -->|新增| H{单条还是批量?}
|
||||
H -->|单条| I[Create]
|
||||
H -->|批量| J[Create slice]
|
||||
Type --> |读取| ReadQ{要几条_}
|
||||
ReadQ --> |1条| OneQ{需要确定性顺序_}
|
||||
OneQ --> |是| FirstNode[First_按主键升序]
|
||||
OneQ --> |否_随机| TakeNode[Take_随机一条]
|
||||
ReadQ --> |N条| FindNode[Find_加Where条件]
|
||||
|
||||
B -->|修改| K{改多少字段?}
|
||||
K -->|1 个| L[Update col, val]
|
||||
K -->|部分| M{需要更新零值?}
|
||||
M -->|是| N[Updates map]
|
||||
M -->|否| O[Updates struct]
|
||||
K -->|全部| P[Save]
|
||||
Type --> |新增| CreateQ{单条还是批量_}
|
||||
CreateQ --> |单条| CreateSingle[Create]
|
||||
CreateQ --> |批量| CreateBatch[Create_slice]
|
||||
|
||||
B -->|删除| Q{启用 SoftDelete?}
|
||||
Q -->|是| R[Delete - 软删除]
|
||||
Q -->|否| S[Delete - 物理删除]
|
||||
Type --> |修改| UpdateQ{改多少字段_}
|
||||
UpdateQ --> |1个| UpdateOne[Update_col_val]
|
||||
UpdateQ --> |部分| ZeroQ{需要更新零值_}
|
||||
ZeroQ --> |是| UpdateMap[Updates_map]
|
||||
ZeroQ --> |否| UpdateStruct[Updates_struct]
|
||||
UpdateQ --> |全部| SaveNode[Save]
|
||||
|
||||
style A fill:#4FC08D,color:#fff
|
||||
style E fill:#3B82F6,color:#fff
|
||||
style I fill:#8B5CF6,color:#fff
|
||||
style P fill:#EC4899,color:#fff
|
||||
style R fill:#F59E0B,color:#000
|
||||
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
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
Reference in New Issue
Block a user