vault backup: 2026-04-28 20:02:42
This commit is contained in:
@@ -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/]]
|
||||||
@@ -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 操作]]
|
||||||
@@ -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 作主键<br/>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-自定义字段类型]]
|
||||||
@@ -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["单条记录<br/>返回 *User"]
|
||||||
|
C --> F["多条记录切片<br/>返回 []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-错误处理]]
|
||||||
@@ -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/]]
|
||||||
Reference in New Issue
Block a user