★ GORM 集成 Gin 的实践要点与常见坑
一句话结论:GORM 集成 Gin 的正确姿势是“全局初始化一次 *gorm.DB(并发安全,内含连接池),main 里组装后依赖注入给 dao/service”,请求维度用 WithContext 传递 ctx 实现超时与取消,而不是每个请求新建连接。最常见的坑集中在:ErrRecordNotFound 判断、更新零值字段、软删除与唯一索引冲突、N+1 查询、动态排序注入、事务没回滚、连接池参数没调导致 MySQL 断连/打爆。
实践要点
- 初始化与池参数:sqlDB, _ := db.DB() 后 SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime(小于 MySQL wait_timeout,默认 8 小时)/SetConnMaxIdleTime;开启慢日志阈值方便排查。
- 每个请求带上 ctx:db.WithContext(c.Request.Context()),DB 操作跟随请求取消。
- 迁移:AutoMigrate 适合开发期;生产建议用版本化迁移工具管理表结构。
- 数据访问收敛到 dao 层,service 依赖 dao,handler 不直接拼 SQL(与目录分层呼应)。
- 用 db.Transaction 回调包事务:回调返回 error 自动回滚,杜绝“忘了 Rollback”。
常见坑与解法
| 坑 | 现象 | 解法 |
|---|---|---|
| 查不到记录判断错误 | 用 err != nil 当“有错误”处理 | errors.Is(err, gorm.ErrRecordNotFound) |
| 更新零值不生效 | Update 传 struct 时 0/"" 被忽略 | 用 map 更新,或 Select/Omit 指定字段 |
| 软删除 + 唯一索引 | 同数据删后无法重建(唯一键撞已删行) | 唯一索引带上 deleted_at,或建普通索引靠业务保证 |
| N+1 查询 | 列表接口几百次 SQL | Preload/Joins 一次取关联 |
| 循环里单条 Insert | 慢、连接占用久 | CreateInBatches 批量写入 |
| Order 拼用户输入 | SQL 注入风险 | 排序字段白名单,禁止直接拼接 |
事务与注入示例
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&Account{}).Where("id = ?", id).
Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
return err // 自动回滚
}
return tx.Create(&Bill{AccountID: id, Amount: amount}).Error
})
// 排序白名单,防注入
sortMap := map[string]string{"created_at": "created_at", "amount": "amount"}
order := sortMap[c.Query("sort")] // 查不到就用默认,绝不直接拼
db.Order(order + " DESC")
追问记忆点
- 追问:事务里忘了 Rollback 会怎样?——连接会被留在事务中占用,回到池里后后续请求可能读到未提交数据或报错;用 Transaction 回调可避免。
- 追问:ErrRecordNotFound 为什么必须用 errors.Is?——GORM 可能返回包装错误,直接 == 比较会漏判。
- 追问:为什么建议 WithContext 传请求 ctx?——客户端断开/超时后 DB 调用随之取消,避免慢查询和连接被无效占用。
- 记忆点:单例 gorm.DB + 依赖注入;事务用回调;零值更新用 map;软删除防唯一冲突;Preload 防 N+1;排序白名单。