From 8d820fc0537a8b8c8a33c879798788a0281c69f6 Mon Sep 17 00:00:00 2001 From: huanghaosheng <386998068@qq.com> Date: Tue, 28 Apr 2026 20:02:42 +0800 Subject: [PATCH] vault backup: 2026-04-28 20:02:42 --- hhs/GORM/01-安装与初始化.md | 206 +++++++++++++++ .../为什么GORM的Create会被自动包裹在事务中的.md | 126 ++++++++++ hhs/GORM/02-模型定义.md | 205 +++++++++++++++ hhs/GORM/03-CRUD 操作.md | 234 ++++++++++++++++++ hhs/GORM/README.md | 63 +++++ 5 files changed, 834 insertions(+) create mode 100644 hhs/GORM/01-安装与初始化.md create mode 100644 hhs/GORM/01-安装与初始化/为什么GORM的Create会被自动包裹在事务中的.md create mode 100644 hhs/GORM/02-模型定义.md create mode 100644 hhs/GORM/03-CRUD 操作.md create mode 100644 hhs/GORM/README.md diff --git a/hhs/GORM/01-安装与初始化.md b/hhs/GORM/01-安装与初始化.md new file mode 100644 index 0000000..65b203c --- /dev/null +++ b/hhs/GORM/01-安装与初始化.md @@ -0,0 +1,206 @@ +--- +tags: [GORM, Go, ORM, 安装, 初始化, 数据库连接] +create time: 2026-04-28 00:00 +--- + +# 安装与初始化 + +## 概述 + +本章介绍 GORM 的安装流程以及三种初始化方式:全局实例、独立 Session 和自定义配置。理解这些基础是后续学习 CRUD 和操作链路的前提。 + +## 安装 + +```bash +go get gorm.io/gorm@latest +go get gorm.io/driver/mysql@latest # MySQL +# go get gorm.io/driver/postgres@latest # PostgreSQL +# go get gorm.io/driver/sqlite@latest # SQLite +``` + +> [!tip] 驱动选择 +> GORM 本身不包含任何数据库驱动,你安装的 `gorm.io/driver/xxx` 包只是适配器(Adapter)。GORM 底层依赖 `database/sql`,所以最终执行 SQL 的还是标准库。 + +## 核心概念 + +在动手之前,先厘清三个容易混淆的对象: + +```mermaid +flowchart LR + Adapter["适配器 Driver"] --> ORM["GORM数据库层"] + ORM --> Builder["查询构建器 *gorm.DB"] + Builder --> Result["最终SQL"] + + style Adapter fill:#EAB308,color:#fff + style ORM fill:#4FC08D,color:#fff + style Builder fill:#3B82F6,color:#fff + style Result fill:#A0AEC0,color:#fff +``` + +| 对象 | 作用 | 类比 | +|------|------|------| +| **Driver** | 将 `*sql.DB` 转化为 `*gorm.DB` | 翻译官 | +| **`*gorm.DB` (DB)** | 全局数据库实例,存储连接池和默认配置 | 数据库连接池 | +| **`*gorm.DB` (Session)** | 通过 `Session()` / `Where()` 等方法产生的新实例,不影响原实例 | 临时会话 | + +## 初始化方式 + +### 方式一:全局单例(最简单) + +```go +import ( + "gorm.io/gorm" + _ "gorm.io/driver/mysql" // 空白导入驱动包 +) + +var DB *gorm.DB + +func InitDB() error { + dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" + var err error + DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) + return err +} +``` + +> [!warning] 全局变量陷阱 +> 虽然方便,但全局 `*gorm.DB` 在高并发场景下可能成为瓶颈。**生产环境推荐使用函数级局部变量 + 依赖注入**。 + +### 方式二:函数内局部实例(推荐) + +```go +func NewGORMInstance() (*gorm.DB, error) { + dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" + return gorm.Open(mysql.Open(dsn), &gorm.Config{}) +} +// 每次请求或每个服务组件持有一个实例 +``` + +### 方式三:函数内局部实例 + 连接池配置(生产推荐) + +```go +func NewDB(dsn string) (*gorm.DB, error) { + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // 调试时开启 SQL 日志 + }) + if err != nil { + return nil, fmt.Errorf("connect db failed: %w", err) + } + + sqlDB, _ := db.DB() // 获取底层 *sql.DB 用于调优连接池 + sqlDB.SetMaxOpenConns(100) // 最大打开连接数 + sqlDB.SetMaxIdleConns(20) // 最大空闲连接数 + sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间,避免被 MySQL 8h 超时断开 + + return db, nil +} + +// ---- 依赖注入使用 ---- +type UserService struct { + db *gorm.DB +} + +func NewUserService(db *gorm.DB) *UserService { + return &UserService{db: db} +} + +func (s *UserService) List() ([]User, error) { + var users []User + return users, s.db.Find(&users).Error // Find 返回 *gorm.DB,取 .Error 得错误 +} +``` + +> [!tip] 为什么不用全局变量? +> | 全局变量 | 局部 + DI | +> |---------|----------| +> | 方便但难以测试 | 易 mocking、可并行测试 | +> | 隐藏依赖关系 | 接口清晰,一眼看出需要什么 | +> | 并发修改 config 互相影响 | 各组件配置独立 | +> +> `*gorm.DB` 本身是 goroutine-safe 的,**共享同一个实例不会有问题**。需要的是「一个实例共享」而非「每个请求新建」。 + +## 关键配置项 `gorm.Config` + +| 字段 | 类型 | 说明 | 默认值 | +|------|------|------|--------| +| `DefaultPageSize` | `int` | 分页默认 Limit | 0(无限制) | +| `SkipDefaultTransaction` | `bool` | 是否跳过默认事务包装 | `false` | +| `DisableForeignKeyConstraintWhenMigrating` | `bool` | 迁移时是否忽略外键约束 | `false` | +| `IgnoreRelationshipsWhenMigration` | `bool` | 迁移时是否忽略关联定义 | `false` | +| `PrepareStmt` | `bool` | 是否启用预编译语句缓存 | `false` | +| `TranslateError` | `bool` | 是否将错误转为 GORM 格式 | `false` | +| `Logger` | `logger.Interface` | 自定义日志输出器 | `Panic` | +| `NowFunc` | `func() time.Time` | 覆盖当前时间(测试用) | `time.Now` | + +### 调试模式配置示例 + +```go +db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), // 开启详细 SQL 日志 +}) +// 输出示例:[info] ... [0.12ms] [rows:-] SELECT * FROM users +``` + +## 连接池调优(`\*sql.DB`) + +上面的方式三已经展示了基本用法,这里展开说明每个参数的含义和推荐值: + +| 方法 | 作用 | 推荐值 | 说明 | +|------|------|--------|------| +| `SetMaxOpenConns(n)` | 最大打开连接数(含在用 + 空闲) | 根据 QPS 评估 | 不超过 MySQL `max_connections` | +| `SetMaxIdleConns(n)` | 最大空闲连接数 | CPU 核数或 10~20 | 越多越能应对突发流量 | +| `SetConnMaxLifetime(d)` | 连接最大存活时间 | 5~10 分钟 | **必须**小于 MySQL 的 `wait_timeout`(默认 8h),否则 GORM 拿到被服务端断开的连接会报错 | +| `SetConnMaxIdleTime(d)` | 空闲连接回收时间(Go 1.15+) | 5 分钟 | 减少闲置连接占用 | + +> [!warning] 常见踩坑 +> - **忘记设 `SetConnMaxLifetime`** → 连接池中存活几年的旧连接,MySQL 早已断开,GORM 复用时报 `server has gone away`。 +> - **`MaxOpenConns = 0`**(默认)→ 无上限,压测时可能打满数据库连接导致其他业务受影响。 +> - **`MaxIdleConns > MaxOpenConns`** → panic,空闲连接不能超过最大打开数。 + +## DSN 模板速查 + +| 数据库 | 驱动包 | DSN 示例 | +|--------|--------|----------| +| MySQL | `gorm.io/driver/mysql` | `user:pass@tcp(host:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local` | +| PostgreSQL | `gorm.io/driver/postgres` | `host=localhost user=gorm password=gorm dbname=gorm port=5432 sslmode=require TimeZone=Asia/Shanghai` | +| SQLite | `gorm.io/driver/sqlite` | `/path/to/db.sqlite`(只需文件路径) | +| SQL Server | `gorm.io/driver/mssql` | `sqlserver://user:pass@host:1433?database=dbname` | + +## GORM 操作链路概览 + +```mermaid +flowchart TD + Init["创建数据库实例"] --> Model["定义模型结构体"] + Model --> Migrate["迁移建表 AutoMigrate"] + Migrate --> Operate["CRUD 数据操作"] + Operate --> Tx["事务提交或回滚"] + Operate --> Return["返回结果"] + + style Init fill:#4FC08D,color:#fff + style Migrate fill:#EAB308,color:#fff + style Operate fill:#3B82F6,color:#fff + style Tx fill:#EF4444,color:#fff +``` + +## 默认事务机制说明 + +```go +db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + SkipDefaultTransaction: true, // 跳过默认事务包装 +}) +``` + +启用后,单次 `Create` / `Update` / `Delete` 操作不再自动包裹事务,性能提升约 10%~20%。**但代价是失去了操作原子性保障**,如果业务逻辑本身只包含单个 SQL 语句且不需要回滚,可以考虑开启。 + +## GORM 思考题 + +> [!question] 为什么 GORM 的 `Create` 操作会被自动包裹在事务中?这样做有什么利弊? +> +> **详见**:[[01-安装与初始化/为什么GORM的Create会被自动包裹在事务中的]] + +## 关联笔记 + +- [[01-安装与初始化/为什么GORM的Create会被自动包裹在事务中的]] +- [[02-模型定义]] +- [[03-CRUD 操作]] +- [[hhs/Go/]] diff --git a/hhs/GORM/01-安装与初始化/为什么GORM的Create会被自动包裹在事务中的.md b/hhs/GORM/01-安装与初始化/为什么GORM的Create会被自动包裹在事务中的.md new file mode 100644 index 0000000..ee3af7b --- /dev/null +++ b/hhs/GORM/01-安装与初始化/为什么GORM的Create会被自动包裹在事务中的.md @@ -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 操作]] diff --git a/hhs/GORM/02-模型定义.md b/hhs/GORM/02-模型定义.md new file mode 100644 index 0000000..5abf47b --- /dev/null +++ b/hhs/GORM/02-模型定义.md @@ -0,0 +1,205 @@ +--- +tags: [GORM, Go, ORM, 模型, struct tag, 字段映射, 主键, 表名] +create time: 2026-04-28 00:00 +--- + +# 模型定义 + +## 概述 + +模型(Model)是 GORM 操作的核心入口——一个普通的 Go struct 被 GORM「看见」后,就可以直接进行数据库操作。本章讲解如何通过 struct tag、命名约定和接口来自定义 GORM 对模型的解读。 + +## 什么是 Model? + +> [!definition] Model +> Model 是一个包含以下任一条件的 struct: +> 1. 有主键字段 +> 2. 定义了 `TableName()` 方法 +> 3. 被 `AutoMigrate`、`Create`、`Where` 等方法引用 + +```go +type User struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:64;not null"` +} +// 这就是一个最简单的 GORM Model +``` + +## 表名规则 + +### 默认命名策略 + +GORM 使用 `Table` 方法来推导表名: + +| 代码行为 | 结果 | +|----------|------| +| 默认 | struct 名的蛇形复数形式(`User` → `users`) | +| 实现 `TableName() string` | 返回的字符串 | +| 使用 `db.Table("xxx")` | 查询时使用的表名(不会修改 Model 的 TableName) | + +```go +func (User) TableName() string { + return "sys_user" // 显式指定表名 +} +``` + +> [!tip] 多环境表名 +> 不同环境下表名前缀不同?可以用环境变量 + `TableName()` 实现动态切换: +> ```go +> func (User) TableName() string { +> if os.Getenv("ENV") == "test" { +> return "test_sys_user" +> } +> return "sys_user" +> } +> ``` + +## 字段 Tag 详解 + +### 完整 Tag 语法 + +``` +gorm:"column:name;type:bigint;not null;default:0;uniqueIndex;index;comment:用户ID" +``` + +| Tag 关键字 | 作用 | 示例 | +|------------|------|------| +| `column` | 映射列名 | `column:user_id` | +| `type` | 列类型覆盖 | `type:varchar(128)` | +| `not null` | NOT NULL 约束 | `not null` | +| `default` | 默认值 | `default:'unknown'` | +| `primaryKey` | 主键 | `primaryKey` | +| `autoIncrement` | 自增 | `autoIncrement` | +| `uniqueIndex` | 唯一索引 | `uniqueIndex:idx_name` | +| `index` | 普通索引 | `index:idx_status` | +| `comment` | 注释(MySQL) | `comment:用户名` | +| `<-` | 字段写入控制 | `<-:true` / `<-:false` / `<-:create` / `<-:update` | +| `->` | 只读/只写 | `->:true`(只读) `/ <-:false`(不写入) | +| `-` | 忽略此字段 | `-` | + +### 写入权限控制 + +```go +type Product struct { + ID uint `gorm:"primaryKey;autoIncrement"` + Name string `gorm:"not null;<-:create"` // 仅创建时可写入 + Price float64 `gorm:"not null;<-:true;->:true"` // 读写均可(默认) + CreatedAt time.Time `gorm:"<-:create;->:true;autoTime"` // 仅创建时自动填充 + UpdatedAt time.Time `gorm:"<-:update;->:true;autoTime"` // 仅更新时自动填充 + ViewCount int `gorm:"->:false;<-:false"` // 完全忽略(非 DB 字段) +} +``` + +> [!example] 方向记忆法 +> `<-` 表示数据**流向数据库**(写入),`->` 表示数据**从数据库流出**(读取)。箭头方向就是数据的方向。 + +## 字段类型映射 + +| Go 类型 | 推荐 DB 类型 | 说明 | +|---------|-------------|------| +| `int`, `int64` | BIGINT | GORM 默认以 `int64` 处理 | +| `uint`, `uint64` | BIGINT UNSIGNED | — | +| `string` | VARCHAR(n) | 需手动指定 size | +| `bool` | TINYINT(1) / BOOLEAN | — | +| `float64` | DOUBLE | — | +| `time.Time` | DATETIME / TIMESTAMP | 配合 `autoTime` tag | +| `[]byte` | BLOB / BYTEA | 二进制数据 | +| `json.RawMessage` | JSON | JSON 序列化字段 | + +### `autoTime` 自动时间戳 + +```go +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"` // 更新时自动填充 +} +``` + +> [!note] createTime / updateTime +> 这两个子 tag 是 GORM v2 的增强功能,配合 `autoTime` 使用。单独 `autoTime` 只更新时间,加上 `createTime` 后才会在插入时设置 CreatedAt。 + +## 主键策略 + +### 默认策略:ID uint + +```go +type User struct { + ID uint // GORM 默认将名为 ID 的字段视为主键 + Name string +} +``` + +### UUID 主键 + +```go +import "github.com/google/uuid" + +type Order struct { + ID uuid.UUID `gorm:"type:char(36);primaryKey"` + Code string `gorm:"uniqueIndex;not null"` +} + +// 在 Create 前生成 UUID +order.ID = uuid.New() +db.Create(&order) +``` + +### 复合主键 + +```go +type OrderItem struct { + OrderID uint `gorm:"primaryKey"` + ProductID uint `gorm:"primaryKey"` + Quantity int `gorm:"not null"` +} +// 生成表结构:PRIMARY KEY (order_id, product_id) +``` + +> [!tip] 复合主键注意事项 +> 使用复合主键时,`First()` 和 `Take()` 将无法工作(因为它们期望单主键),必须使用 `Where()` 精确定位。 + +## 自定义类型作为字段 + +当内置类型不够用时,可以实现 GORM 接口: + +```go +type MyType int + +func (m MyType) GORMDataType() string { + return "INT" +} + +func (m MyType) Value() (driver.Value, error) { + return int(m), nil // ScannerValuer 接口的 Value 方法 +} +``` + +## 模型设计流程图 + +```mermaid +flowchart TD + A[定义 struct] --> B{有无 ID 字段?} + B -->|有| C["ID 作主键
uint/int64"] + B -->|无| D{自定义 TableName?} + D -->|实现了| E[按 TableName 返回值] + D -->|未实现| F["struct 名蛇形复数"] + C --> G[添加字段 tag] + E --> G + G --> H{需要特殊类型?} + H -->|是| I[实现 GORMDataType / Valuer] + H -->|否| J[完成] + I --> J + + style A fill:#4FC08D,color:#fff + style C fill:#3B82F6,color:#fff + style I fill:#EAB308,color:#fff + style J fill:#A0AEC0,color:#fff +``` + +## 关联笔记 + +- [[01-安装与初始化]] +- [[03-CRUD 操作]] +- [[12-自定义字段类型]] diff --git a/hhs/GORM/03-CRUD 操作.md b/hhs/GORM/03-CRUD 操作.md new file mode 100644 index 0000000..39a0172 --- /dev/null +++ b/hhs/GORM/03-CRUD 操作.md @@ -0,0 +1,234 @@ +--- +tags: [GORM, Go, ORM, CRUD, 增删改查, First, Find, Create, Update, Delete] +create time: 2026-04-28 00:00 +--- + +# CRUD 操作 + +## 概述 + +CRUD 是最基础的数据库操作。GORM 提供了语义清晰的五个核心查询方法(First / Take / Find / Get / Last)和多种更新删除 API。掌握它们就能覆盖日常 80% 的数据交互需求。 + +## 五大查询方法 + +```mermaid +flowchart LR + A[db.Model(&User{})] --> B{First / Take / Last} + A --> C[Find - 批量查询] + A --> D[Get - 封装版 First] + B --> E["单条记录
返回 *User"] + C --> F["多条记录切片
返回 []User"] + D --> E + + 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 +``` + +### 方法对比 + +| 方法 | 返回类型 | 行为 | 是否需要 Where | +|------|----------|------|----------------| +| `First` | 单条指针 | 按主键排序取第一条,**必须有条件** | ✅ 必须 | +| `Take` | 单条指针 | 随机取一条(ORDER BY 随机),**必须有条件** | ✅ 必须 | +| `Last` | 单条指针 | 按主键倒序取最后一条 | ✅ 必须 | +| `Find` | 切片 | 查询所有匹配行 | ❌ 可选 | +| `Get` | 单条指针 | `First` 的封装,自动处理空结果 | 同 First | + +### First vs Take 的区别 + +```go +// First: 按主键升序取第一条(确定性) +db.First(&user) // SELECT * FROM users ORDER BY primary_key LIMIT 1; WHERE ? + +// Take: 无序取任意一条(用于随机抽样) +db.Take(&user) // SELECT * FROM users LIMIT 1; +``` + +### 实际用法示例 + +```go +// 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) { + // 记录不存在 +} +``` + +> [!danger] 常见误区 +> `Find` 不带 `Where` 会查询全表!数据量大时会导致 OOM。始终记得加过滤条件,或用 `Limit` 兜底。 + +## 创建(Create) + +```go +// 单条插入 +user := User{Name: "Alice", Age: 30} +result := db.Create(&user) + +// result.RowsAffected — 影响行数 +// result.Error — 错误 + +// 批量插入(至少两条才生效批量优化) +users := []User{ + {Name: "Bob", Age: 25}, + {Name: "Charlie", Age: 35}, +} +db.Create(&users) // INSERT INTO users ... VALUES (...), (...), (...) +``` + +> [!tip] 批量插入上限 +> GORM 内部会将批量 insert 拆分为每批约 256 条,避免单次 SQL 过大。如需调整,可通过 `db.Session(&gorm.Session{FullSaveRecords: true})` 控制行为。 + +### 插入后获取自增 ID + +```go +db.Create(&user) +fmt.Println(user.ID) // 插入后 ID 自动回填到结构体 +``` + +## 更新(Update) + +GORM 提供多种更新粒度,从单个字段到全量替换: + +### 方法速查表 + +| 方法 | 签名 | 行为 | +|------|------|------| +| `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 | + +### 详细示例 + +```go +// 1. Update - 单字段 +db.Model(&user).Update("name", "Bob") +// UPDATE users SET name='Bob', updated_at=... WHERE id=... + +// 2. Updates - 多字段(map 方式,零值也会被更新) +db.Model(&user).Updates(map[string]any{ + "name": "Alice", + "age": 0, // 注意:0 也会被写入 + "role": "admin", +}) + +// 3. Updates - 多字段(struct 方式,只更新非零值字段!) +db.Model(&user).Updates(User{Name: "Alice", Role: "admin"}) +// 只有 Name 和 Role 被更新,Age 保持不变 + +// 4. Save - 全量覆盖(全部字段都写回去) +db.Save(&user) +// UPDATE users SET name='Alice', age=30, role='admin', ... WHERE id=... +``` + +> [!warning] Zero Values 陷阱 +> `Updates` 用 struct 传入时,**零值字段("" 0 false)不会被更新**。如果需要更新零值,改用 `map[string]any` 方式: +> ```go +> db.Model(&user).Updates(map[string]any{"age": 0}) // ✅ 能更新为 0 +> ``` + +### 批量更新 + +```go +// 批量更新满足条件的行 +db.Model(&User{}).Where("status = ?", "active").Update("role", "vip") +// UPDATE users SET role='vip' WHERE status='active'; +``` + +## 删除(Delete) + +```go +// 根据主键删除 +db.Delete(&user, 10) // DELETE FROM users WHERE id = 10 + +// 批量删除 +db.Where("age < ?", 18).Delete(&User{}) + +// 软删除(如果模型包含 DeletedAt) +db.Delete(&user) // 不是 DELETE,而是 UPDATE ... SET deleted_at=NOW() +``` + +> [!info] 软删除 vs 物理删除 +> 物理 `DELETE FROM` 不可逆且丢失审计痕迹。如果你启用了 `SoftDelete`(模型含 `DeletedAt` 字段),默认执行软删除。 +> +> 想强制执行物理删除?使用 `Unscoped`: +> ```go +> db.Unscoped().Delete(&user, 10) // 真正的 DELETE +> ``` + +## 错误处理 + +```go +result := db.First(&user, 100) +if errors.Is(result.Error, gorm.ErrRecordNotFound) { + fmt.Println("用户不存在") +} else if result.Error != nil { + fmt.Println("数据库错误:", result.Error) +} +``` + +| 错误常量 | 含义 | +|----------|------| +| `gorm.ErrRecordNotFound` | 查询结果为空 | +| `gorm.ErrInvalidData` | 插入/更新的数据无效 | +| `gorm.ErrTxAlreadyCommitted` | 事务已提交 | +| `gorm.ErrSavedValueNotNull` | 保存了非空字段的零值 | + +## CRUD 决策流程图 + +```mermaid +flowchart TD + A[收到数据操作请求] --> B{操作类型?} + B -->|读取| C{要几条?} + C -->|1 条| D{需要确定性顺序?} + D -->|是| E[First - 按主键升序] + D -->|否/随机| F[Take - 随机一条] + C -->|N 条| G[Find + Where 条件] + + B -->|新增| H{单条还是批量?} + H -->|单条| I[Create] + H -->|批量| J[Create slice] + + B -->|修改| K{改多少字段?} + K -->|1 个| L[Update col, val] + K -->|部分| M{需要更新零值?} + M -->|是| N[Updates map] + M -->|否| O[Updates struct] + K -->|全部| P[Save] + + B -->|删除| Q{启用 SoftDelete?} + Q -->|是| R[Delete - 软删除] + Q -->|否| S[Delete - 物理删除] + + 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 +``` + +## 关联笔记 + +- [[01-安装与初始化]] +- [[02-模型定义]] +- [[04-条件查询]] +- [[10-软删除]] +- [[14-错误处理]] diff --git a/hhs/GORM/README.md b/hhs/GORM/README.md new file mode 100644 index 0000000..19eec1d --- /dev/null +++ b/hhs/GORM/README.md @@ -0,0 +1,63 @@ +--- +tags: [GORM, Go, ORM, Database] +create time: 2026-04-28 00:00 +--- + +# GORM 知识库 + +## 概述 + +GORM 是 Go 生态中最流行的 ORM 库,本知识库系统整理 GORM 的核心概念、进阶用法和工程实践。 + +## 目录索引 + +### 1. 基础篇 + +- **[01-安装与初始化](./01-安装与初始化)** — 依赖安装、DB 连接配置、全局配置项 +- **[02-模型定义](./02-模型定义)** — struct tag 映射、字段类型、主键策略、表名规则 +- **[03-CRUD 操作](./03-CRUD 操作)** — Create / First / Take / Find / Get 五大方法 + +### 2. 查询篇 + +- **[04-条件查询](./04-条件查询/)** — Where 链式调用、Between / In / Like / 原生 SQL +- **[05-关联查询](./05-关联查询/)** — Preload / Joins、HasOne / HasMany / BelongsTo / ManyToMany +- **[06-排序与分页](./06-排序与分页/)** — Order / Limit / Offset / Paginate +- **[07-子查询与分组](./07-子查询与分组/)** — Group / Having / SubQuery / 嵌套查询 + +### 3. 进阶篇 + +- **[08-事务管理](./08-事务管理/)** — Tx 对象、Commit / Rollback、嵌套事务 +- **[09-钩子函数](./09-钩子函数/)** — Before / After Save、Create、Update、Delete、Find +- **[10-软删除](./10-软删除/)** — SoftDelete 原理、Unscoped 强制查询 +- **[11-批量操作](./11-批量操作/)** — Bulk Insert / Update、Callback 机制 +- **[12-自定义字段类型](./12-自定义字段类型/)** — Scanner / Valuer 接口、JSON 字段 +- **[13-多数据库支持](./13-多数据库支持/)** — MySQL / PostgreSQL / SQLite / SQL Server + +### 4. 工程实践篇 + +- **[14-错误处理](./14-错误处理/)** — RecordNotFound、ErrRelatedExists、自定义错误判断 +- **[15-性能优化](./15-性能优化/)** — N+1 问题排查、Preload 策略、预编译语句 +- **[16-日志与调试](./16-日志与调试/)** — 慢查询监控、SQL 日志格式化 +- **[17-迁移工具](./17-迁移工具/)** — AutoMigrate / Migrator、版本化迁移策略 + +## 核心架构图 + +```mermaid +graph LR + A[GORM DB] --> B[Session] + B --> C[Clauses] + C --> D[Standalone Mode] + B --> E[Context] + E --> F[Hooks] + F --> G[Preload] + G --> H[Resolver] + H --> I[Driver] + I --> J[(Database)] + + style A fill:#4FC08D,color:#fff + style J fill:#A0AEC0,color:#fff +``` + +## 关联笔记 + +- [[hhs/Go/]]