GORM v2 最佳实践与性能优化。适用于:代码审查、慢查询优化、N+1、连接池、 事务管理、分库分表、Prometheus/OTel监控、Session安全、Clause/Upsert、 缓存集成、BaseModel脚手架、SQL→struct生成、多租户隔离。 触发词:GORM、数据库慢、加索引、写struct、分库分表、Preload、FOR UPDATE、 Session累积、goroutine db、缓存、多租户、迁移。
Scanned 9/6/2026
Install to Claude Code
npx -y skills add LeoYeAI/openclaw-master-skills --skill gorm-expert-skill --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Gorm Expert Skill?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/leoyeai-gorm-expert-skill)More formats (shields.io, HTML) on the badges page.
---
name: gorm-expert
description: >
GORM v2 最佳实践与性能优化。适用于:代码审查、慢查询优化、N+1、连接池、
事务管理、分库分表、Prometheus/OTel监控、Session安全、Clause/Upsert、
缓存集成、BaseModel脚手架、SQL→struct生成、多租户隔离。
触发词:GORM、数据库慢、加索引、写struct、分库分表、Preload、FOR UPDATE、
Session累积、goroutine db、缓存、多租户、迁移。
---
# GORM 使用与性能优化 Skill
## 脚本工具(优先用脚本,减少 token 消耗)
> 用户提供代码/SQL/参数时,**先跑脚本**,只输出脚本结果 + 针对性说明。
> 所有脚本仅依赖 Python 3.8+,不调用任何外部 API 或凭证。
| 场景 | 脚本 | 用法示例 |
|------|------|---------|
| Go 代码审查 | `scripts/analyze_gorm.py` | `python3 scripts/analyze_gorm.py - <<< "代码"`(R1–R27;CI 用 `--format json`) |
| CREATE TABLE → struct | `scripts/gen_model.py` | `echo "CREATE TABLE..." \| python3 scripts/gen_model.py -`(PG 加 `--dialect pg`) |
| 连接池计算 | `scripts/pool_advisor.py` | `python3 scripts/pool_advisor.py --qps 500 --avg-latency-ms 20 --app-instances 4` |
| SQL 性能分析 | `scripts/query_explain.py` | `python3 scripts/query_explain.py "SELECT * FROM ..."` |
| 迁移 SQL 生成 | `scripts/migration_gen.py` | `python3 scripts/migration_gen.py old.go new.go --table users` |
| Scope 生成 | `scripts/scope_gen.py` | `python3 scripts/scope_gen.py model.go --tenant --paginate` |
| Benchmark 模板 | `scripts/bench_template.py` | 默认 stdout;写文件加 `--output bench_test.go` |
| 项目初始化 | `scripts/init_project.py` | 先 `--dry-run` 预览,再 `--output ./internal/dbcore` 写入 |
---
## 核心原则
1. **禁止物理外键,必须使用逻辑外键** — 所有关联字段只建索引,不建 FK CONSTRAINT;`gorm.Config` 必须设置 `DisableForeignKeyConstraintWhenMigrating: true`;若已有 `foreignKey` tag 必须加 `constraint:false`
2. **先测量,再优化** — `db.Debug()` 或自定义 Logger 定位慢 SQL
3. **最小数据传输** — 只 Select 需要的字段,只查需要的行
4. **减少 Round-trip** — 批量操作、Preload vs 懒加载权衡
5. **连接复用** — 正确配置连接池,避免频繁开关连接
---
## 1. 初始化与连接池
```go
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, // 写性能 +30%
PrepareStmt: true, // SQL 编译缓存
DisableForeignKeyConstraintWhenMigrating: true, // 禁物理 FK
Logger: logger.New(writer, logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn,
}),
})
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(20) // MaxOpen 的 20%
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
```
> 连接池参数建议:`python3 scripts/pool_advisor.py --qps 500 --avg-latency-ms 20`
**ID 生成策略**(使用 dbcore 脚手架时配置):
```go
// Snowflake(默认,零依赖)
dbcore.SetIDGenerator(dbcore.NewSnowflakeGenerator(nodeID))
// Leaf-Segment(高并发推荐,严格递增,需 MySQL 号段表)
dbcore.SetIDGenerator(dbcore.NewLeafSegmentGenerator(db, "order"))
// UUID(无需协调,不可排序)
dbcore.SetIDGenerator(dbcore.NewUUIDGenerator())
```
> 策略选型、时钟回拨保护、K8s nodeID 管理详见 `references/id-generation.md`
**多租户配置**(按需开启,默认关闭):
```go
dbcore.SetConfig(dbcore.Config{
TenantEnabled: true, // 开启多租户隔离
TenantField: "tenant_id", // 租户字段名
TenantStrict: true, // 严格模式:无租户时拒绝查询
TenantExtractor: func(ctx context.Context) string {
if tid, ok := ctx.Value("tenant_id").(string); ok { return tid }
return ""
},
})
// 开启后所有 BaseModel CRUD 自动注入 tenant_id 条件,无需手动拼接
// 不需要多租户时,不调用 SetConfig 即可(TenantEnabled 默认 false)
```
**脚手架初始化**:
```bash
python3 scripts/init_project.py --output ./internal/dbcore # 写入
python3 scripts/init_project.py --output ./internal/dbcore --dry-run # 仅预览
python3 scripts/init_project.py --output ./internal/dbcore --example # 含示例
```
---
## 2. 查询优化
### 2.1 只查需要的字段
```go
// ❌ SELECT *
db.Find(&users)
// ✅ SELECT id, name, email
db.Select("id", "name", "email").Find(&users)
// ✅ 投影到小 struct
db.Model(&User{}).Select("id", "name").Scan(&dtos)
```
### 2.2 避免 N+1 查询
```go
// ❌ N+1:循环内查 DB
for _, u := range users { db.Where("user_id=?", u.ID).Find(&u.Orders) }
// ✅ Preload:两次查询
db.Preload("Orders").Find(&users)
// ✅ Joins:一次 JOIN,按关联字段过滤
db.Joins("JOIN orders ON orders.user_id=users.id AND orders.status=?","paid").Find(&users)
// ✅ 带条件 Preload
db.Preload("Orders", "status=?", "paid").Preload("Orders.Items").Find(&users)
```
> Preload vs Joins 选型详见 `references/association.md`
### 2.3 分批处理大数据集
```go
// ✅ FindInBatches:每批 500 条
db.Model(&User{}).FindInBatches(&batch, 500, func(tx *gorm.DB, n int) error {
process(batch); return nil
})
// ✅ 游标分页(大表推荐,避免 OFFSET 退化)
var lastID uint
for {
var users []User
db.Where("id > ?", lastID).Order("id").Limit(500).Find(&users)
if len(users) == 0 { break }
lastID = users[len(users)-1].ID
}
```
### 2.4 聚合与子查询
```go
// 分组统计
db.Model(&Order{}).Select("user_id, SUM(amount) as total").
Group("user_id").Having("SUM(amount) > ?", 1000).Scan(&results)
// 去重
db.Distinct("status").Model(&Order{}).Pluck("status", &statuses)
// 子查询
subQuery := db.Model(&Order{}).Select("user_id").Where("amount > ?", 1000)
db.Where("id IN (?)", subQuery).Find(&users)
```
### 2.5 流式读取与索引提示
```go
// 流式读取(超大数据集,逐行处理)
rows, _ := db.Model(&User{}).Where("status = ?", "active").Rows()
defer rows.Close()
for rows.Next() { var u User; db.ScanRows(rows, &u); process(u) }
// 索引提示
db.Clauses(hints.UseIndex("idx_user_name")).Find(&users)
db.Clauses(hints.ForceIndex("idx_created_at").ForOrderBy()).Find(&orders)
```
---
## 3. 写操作优化
### 3.1 批量插入
```go
// ❌ 逐条 INSERT(N 次 Round-trip)
for _, u := range users { db.Create(&u) }
// ✅ 批量插入
db.CreateInBatches(&users, 200)
```
### 3.2 按需更新,不更新零值
```go
// ❌ struct Updates 忽略零值(int=0, bool=false)
db.Model(&user).Updates(User{Name: "new", Age: 0}) // Age=0 被忽略!
// ✅ map 明确指定字段
db.Model(&user).Updates(map[string]any{"name": "new", "age": 0})
// ✅ Select 限制 / Omit 排除
db.Model(&user).Select("name", "email").Updates(&user)
db.Model(&user).Omit("password").Updates(&user)
```
### 3.3 检查 RowsAffected
```go
result := db.Model(&User{}).Where("id = ?", id).Update("status", "banned")
if result.Error != nil { return result.Error }
if result.RowsAffected == 0 { return fmt.Errorf("user %d not found", id) }
```
### 3.4 Upsert
```go
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "updated_at"}),
}).Create(&user)
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&users) // 忽略冲突
```
---
## 4. 事务管理
```go
// 标准事务
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil { return err }
return tx.Model(&stock).Update("qty", gorm.Expr("qty - ?", 1)).Error
})
// 嵌套事务(GORM 自动 SavePoint)
tx.Transaction(func(tx2 *gorm.DB) error { return tx2.Create(&log).Error })
// 悲观锁(FOR UPDATE)
db.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", id).First(&stock)
```
> 乐观锁、CAS、Savepoint、事务陷阱详见 `references/concurrency.md`
---
## 5. 读写分离
```go
db.Use(dbresolver.Register(dbresolver.Config{
Sources: []gorm.Dialector{mysql.Open(writeDSN)},
Replicas: []gorm.Dialector{mysql.Open(read1DSN), mysql.Open(read2DSN)},
Policy: dbresolver.RandomPolicy{},
}).SetMaxOpenConns(50).SetMaxIdleConns(10))
// GORM 自动路由:Find/First → Replica;Create/Update/Delete → Source
db.Clauses(dbresolver.Write).Find(&user) // 强制走主库
```
---
## 6. Scopes 与多租户
```go
// 可复用 Scope
func ActiveUser(db *gorm.DB) *gorm.DB { return db.Where("status = ?", "active") }
func AgeOver(age int) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB { return db.Where("age > ?", age) }
}
db.Scopes(ActiveUser, AgeOver(18)).Find(&users)
```
**多租户隔离**(两种方式,按需选择):
```go
// 方式 1(推荐):通过 dbcore.Config 全局开关,BaseModel 自动注入
// 开启:dbcore.SetConfig(dbcore.Config{TenantEnabled: true, ...})
// 关闭:不调用 SetConfig 或 TenantEnabled: false(默认)
// 详见 §1 初始化配置
// 方式 2:手动 Scope(不使用 dbcore 时适用)
func TenantScope(ctx context.Context) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if tid, ok := ctx.Value("tenant_id").(string); ok && tid != "" {
return db.Where("tenant_id = ?", tid)
}
return db.Where("1 = 0")
}
}
```
> 自动生成:`python3 scripts/scope_gen.py model.go --tenant --paginate`
> 完整示例详见 `references/scopes.md`
---
## 7. Model 设计规范
```go
type Order struct {
gorm.Model
UserID uint `gorm:"not null;index"` // 逻辑外键:只加索引,不建 FK
Status string `gorm:"type:varchar(20);index"`
Amount int64 `gorm:"not null;default:0"`
}
```
**逻辑外键规范**(核心原则 #1):
```go
// ❌ 物理外键:会在 AutoMigrate 时创建 FK CONSTRAINT
User User `gorm:"foreignKey:UserID"`
// ❌ 有 references 但没有 constraint:false
User User `gorm:"foreignKey:UserID;references:ID"`
// ✅ 逻辑外键:显式禁止约束
User User `gorm:"foreignKey:UserID;constraint:false"`
// ✅ 全局禁止(必须写在 gorm.Config 中)
db, _ := gorm.Open(dsn, &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
```
> 物理外键的问题:分库分表不兼容、级联不可控、高并发性能瓶颈、导入迁移困难。
> 详见 `references/base-model-pattern.md`
---
## 8. 调试与性能分析
```go
db.Debug().Find(&users) // 单次打印 SQL
// ToSQL(不执行,只生成 SQL)
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
return tx.Where("id > ?", 100).Limit(10).Find(&users)
})
```
> pprof / Benchmark 详见 `references/observability.md`
---
## 9. 常见坑与反模式
| 问题 | 错误写法 | 正确做法 |
|------|----------|----------|
| 忘记传 Context | `db.Find(&u)` | `db.WithContext(ctx).Find(&u)` |
| struct Updates 丢零值 | `db.Updates(User{Age:0})` | `db.Updates(map[string]any{"age":0})` |
| 大 OFFSET 分页 | `db.Offset(100000).Limit(20)` | 游标分页 `WHERE id > lastID` |
| 前导通配 Like | `LIKE '%foo%'` | 前缀匹配 `LIKE 'foo%'` 或全文索引 |
| 事务内耗时操作 | 事务 + HTTP 调用 | HTTP 移到事务外 |
| 未检查 Error | `db.Find(&u); use u` | `if err := db.Find(&u).Error; err != nil` |
| 物理外键 | 未加 `constraint:false` | 加 `constraint:false` 或全局禁止 |
| Save 全量更新 | `db.Save(&user)` | `db.Model(&u).Updates(map[string]any{...})` |
| 忽略 RowsAffected | 不检查影响行数 | `if result.RowsAffected == 0 { ... }` |
---
## 10. 缓存集成(Cache-Aside)
```go
func (r *UserRepo) GetUser(ctx context.Context, id uint) (*User, error) {
key := fmt.Sprintf("user:%d", id)
if val, err := r.rdb.Get(ctx, key).Bytes(); err == nil {
var u User; json.Unmarshal(val, &u); return &u, nil
}
// singleflight 防击穿
res, err, _ := r.group.Do(key, func() (any, error) {
var u User
if err := r.db.WithContext(ctx).First(&u, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
r.rdb.Set(ctx, key, "null", time.Minute) // 防穿透
}
return nil, err
}
ttl := 30*time.Minute + time.Duration(rand.Int63n(int64(6*time.Minute))) // 抖动防雪崩
data, _ := json.Marshal(u)
r.rdb.Set(ctx, key, data, ttl)
return &u, nil
})
// 写操作:更新 DB → 删缓存(不更新缓存)
}
```
> 布隆过滤器防穿透、延迟双删、列表缓存详见 `references/caching.md`
---
## 11. 分库分表(Sharding)
```go
db.Use(sharding.Register(sharding.Config{
ShardingKey: "user_id", NumberOfShards: 64,
PrimaryKeyGenerator: sharding.PKSnowflake,
}, "orders"))
db.Where("user_id = ?", userID).Find(&orders) // 携带分片键 → 自动路由
```
> 分片算法、双写迁移、跨分片查询详见 `references/sharding.md`
---
## 12. 监控与可观测性
```go
db.Use(prometheus.New(prometheus.Config{DBName: "myapp", RefreshInterval: 15}))
db.Use(otelgorm.NewPlugin(otelgorm.WithDBName("myapp"))) // OpenTelemetry
```
> Prometheus 告警、Grafana 仪表盘、pprof 详见 `references/observability.md`
---
## 13. GORM v2 核心机制
### 13.1 Session 与 goroutine 安全
```go
// ❌ 条件累积 + goroutine 数据竞争
base := db.Where("tenant_id = ?", tid)
go func() { base.Find(&list1) }() // 危险!
// ✅ Session 隔离
go func() { base.Session(&gorm.Session{NewDB: true}).Find(&list1) }()
```
> Session 配置项、8 种陷阱详见 `references/session.md`
### 13.2 Clause 系统
```go
db.Clauses(clause.Locking{Strength: "UPDATE"}).First(&stock) // FOR UPDATE
db.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).Find(&tasks) // 任务抢占
db.Clauses(clause.Returning{}).Create(&user) // RETURNING(PostgreSQL)
```
> 完整 Clause 用法、自定义表达式详见 `references/clause.md`
### 13.3 Association 关联操作
```go
db.Preload("Orders", "status = ?", "paid").Find(&users)
db.Preload(clause.Associations).Find(&users) // 预加载所有关联
db.Omit(clause.Associations).Create(&user) // 跳过关联写入
db.Model(&user).Association("Orders").Append(&order) // 添加关联
```
> Preload vs Joins、级联控制、多对多详见 `references/association.md`
### 13.4 Serializer 与自定义类型
```go
type User struct {
Tags []string `gorm:"type:json;serializer:json"` // JSON 自动序列化
Phone string `gorm:"serializer:encrypted"` // 自定义加密
}
```
> 完整实现、GormDataType 接口详见 `references/serializer.md`
### 13.5 Error 处理规范(v2)
```go
errors.Is(err, gorm.ErrRecordNotFound) // ✅ v2 正确写法
// ❌ gorm.IsRecordNotFoundError(err) — v1 API,v2 已移除
// 注意:Find 不触发 ErrRecordNotFound;First/Take/Last 触发
```
---
## 14. 进阶参考
详细专题见 `references/` 目录(按需加载,不要全量读入):
| 文件 | 内容 | 触发时机 |
|------|------|---------|
| `base-model-pattern.md` | BaseModel Bug 修复、游标分页、多租户隔离 | BaseModel、QueryBuilder、分页 |
| `session.md` | Session 机制、goroutine 安全、条件累积 | Session、db 复用、DryRun |
| `clause.md` | Clause 系统(Upsert/FOR UPDATE/RETURNING) | FOR UPDATE、Upsert |
| `association.md` | Preload/Joins、级联控制 | 关联加载、多对多 |
| `serializer.md` | Serializer、自定义类型(枚举/Money/加密) | 字段序列化 |
| `hooks.md` | Hook 执行顺序、性能陷阱 | Hook 使用 |
| `raw-sql.md` | Raw SQL / Scan / Rows | 原生 SQL |
| `indexing.md` | 索引设计、EXPLAIN 验证 | 索引、慢查询 |
| `concurrency.md` | 乐观锁、悲观锁、CAS | 并发冲突、超卖 |
| `testing.md` | sqlmock、SQLite 集成测试 | GORM 测试 |
| `migration.md` | golang-migrate、大表在线 DDL | 数据库迁移 |
| `sharding.md` | 分库分表、分片算法 | 水平拆分 |
| `observability.md` | Prometheus、OTel、Grafana | 监控 |
| `scopes.md` | Scope、分页、多租户 | Scope 用法 |
| `caching.md` | Cache-Aside、防击穿/雪崩/穿透 | Redis 缓存 |
| `soft-delete.md` | 软删除、唯一约束兼容、归档清理 | 软删除 |
| `id-generation.md` | Snowflake/Leaf-Segment/UUID、时钟回拨 | 分布式 ID |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!