Init
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
---
|
||||
tags: [GORM, Go, ORM, 事务, Create, 原子性]
|
||||
create time: 2026-04-28 00:00
|
||||
---
|
||||
|
||||
# 为什么 GORM 的 Create 会被自动包裹在事务中?
|
||||
|
||||
## 概述
|
||||
|
||||
通过剖析 `Create` 操作的底层执行链路,解释 GORM 默认使用事务包装的设计动机、利弊权衡以及性能优化手段。
|
||||
|
||||
## GORM 的一条 Create 不只是 INSERT
|
||||
|
||||
很多开发者习惯这样写:
|
||||
|
||||
```go
|
||||
db.Create(&user) // 看起来很简洁,对吧?
|
||||
```
|
||||
|
||||
但这条看似简单的调用,实际会触发以下完整步骤:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
BeforeHook["BeforeCreate Hook"] --> Insert["INSERT INTO users..."]
|
||||
Insert --> Timestamp["更新时间戳 CreatedAt / UpdatedAt"]
|
||||
Timestamp --> Assoc["关联表处理(Preload 等)"]
|
||||
Assoc --> AfterHook["AfterCreate Hook"]
|
||||
|
||||
style BeforeHook fill:#EAB308,color:#fff
|
||||
style Insert fill:#3B82F6,color:#fff
|
||||
style Timestamp fill:#4FC08D,color:#fff
|
||||
style Assoc fill:#A0AEC0,color:#fff
|
||||
style AfterHook fill:#EAB308,color:#fff
|
||||
```
|
||||
|
||||
| 环节 | 说明 |
|
||||
|------|------|
|
||||
| **BeforeCreate Hook** | 用户自定义的预处理逻辑,可能修改字段值 |
|
||||
| **INSERT** | 核心 SQL 写入操作 |
|
||||
| **自动时间戳** | 标记了 `autoCreateTime` / `autoUpdateTime` 的字段会自动注入当前时间,可能产生 UPDATE |
|
||||
| **关联表处理** | Preload 嵌套创建的子模型会产生额外的 INSERT |
|
||||
| **AfterCreate Hook** | 后置逻辑,例如发送通知、记录审计日志 |
|
||||
| **主键回填** | PostgreSQL SERIAL / SQLite AUTOINCREMENT 需要二次查询获取自增 ID |
|
||||
|
||||
这些步骤分散在执行链的不同阶段,GORM 把它们全部放进同一个事务里,确保**要么全部成功、要么全部回滚**。这就是 "安全优先" 哲学的基础。
|
||||
|
||||
## 优势
|
||||
|
||||
### 1. 原子性保证
|
||||
|
||||
所有相关操作处于同一事务边界内,中间任何一步失败都会整体回滚:
|
||||
|
||||
```go
|
||||
// 即使 BeforeCreate 里改了多个字段,INSERT 后又要更新软删除标记,
|
||||
// 任何一个环节出错都不会留下半写状态
|
||||
db.Create(&Order{
|
||||
Items: []Item{{Name: "Widget"}, {Name: "Gadget"}}, // 嵌套创建
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 与手动事务无缝兼容
|
||||
|
||||
如果业务逻辑本身就需要事务,GORM 不会额外嵌套一层,而是复用外层事务:
|
||||
|
||||
```go
|
||||
db.Transaction(func(tx *gorm.DB) error {
|
||||
tx.Create(&user) // 不产生新事务,复用外层 tx
|
||||
tx.Create(&profile) // 同上
|
||||
return nil // nil = commit
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 降低误操作门槛
|
||||
|
||||
新手不需要理解"什么时候该手动开事务",默认就获得基本的一致性保障。
|
||||
|
||||
## 劣势
|
||||
|
||||
### 1. 性能开销
|
||||
|
||||
每条 `Create` / `Update` / `Delete` 都多了一层 BEGIN / COMMIT,高吞吐场景下累积明显:
|
||||
|
||||
> 实测差异:百万级 QPS 下,不开启事务包装可节省约 10%~20% 的写入延迟。
|
||||
|
||||
### 2. 长事务锁风险
|
||||
|
||||
如果 Hook 中包含慢查询或外部 HTTP 调用,事务持锁时间被拉长:
|
||||
|
||||
```go
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
// 这会让整个事务等待网络请求返回 — 非常危险!
|
||||
resp, _ := http.Get("https://api.example.com/validate")
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 开发者心智负担
|
||||
|
||||
不理解底层机制的人可能会困惑:"为什么我一条简单的 Create 这么慢?" 也可能错误地认为完全不需要关心事务管理。
|
||||
|
||||
## 如何关闭默认事务
|
||||
|
||||
GORM 提供了 `SkipDefaultTransaction` 配置项:
|
||||
|
||||
```go
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
SkipDefaultTransaction: true, // 跳过默认事务包装
|
||||
})
|
||||
```
|
||||
|
||||
开启后,单次 `Create` / `Update` / `Delete` 不再自动包裹事务。**适用于只涉及单条 SQL 且不需要回滚的场景**(无 Hook、无关联插入、无时间戳更新)。
|
||||
|
||||
## 最佳实践对照表
|
||||
|
||||
| 场景 | 建议 |
|
||||
|------|------|
|
||||
| 单条简单 INSERT,无 Hook、无关联、无时间戳 | 可设置 `SkipDefaultTransaction: true` |
|
||||
| 需要多步复合操作(如创建订单 + 扣库存) | 用 `db.Transaction()` 显式控制范围 |
|
||||
| Hook 中有网络请求或慢查询 | 把 IO 移到事务之外,避免拉宽事务窗口 |
|
||||
| 批量插入 `CreateInBatches` | 内部已按批次分组事务,无需额外处理 |
|
||||
| Preload 嵌套创建子模型 | 保留默认行为,否则关联数据无法原子落盘 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[01-安装与初始化]]
|
||||
- [[hhs/GORM/03-CRUD 操作]]
|
||||
Reference in New Issue
Block a user