不能在 handler 里直接调用 gorm 方法,因会导致事务失控、测试困难、错误处理混乱、逻辑耦合;应分层:handler 负责请求响应,service 封装业务规则并控制事务,repository 封装数据操作并统一错误,model 仅定义结构与 tag。

别直接在 handler 里写 db.Create() 或 db.First() —— 模型操作必须分层,否则事务、测试、错误处理全崩。
为什么不能在 handler 里直接调用 GORM 方法
看似省事,实则把数据访问逻辑和 HTTP 协议细节耦合死:事务无法跨 handler 控制;单元测试时没法 mock 数据层;错误码(如 record not found)混在 handler 里,一改逻辑就得动路由;更麻烦的是,db.Transaction() 必须包裹完整业务流程,放在 handler 里根本拿不到 service 层的上下文。
- handler 只负责解析请求、校验参数、返回响应,不碰数据库
- service 层封装业务规则(比如“创建 todo 前检查用户配额”),并决定是否启事务
- repository 层才真正调用
db.Create()、db.Where().Find()等 GORM 方法 - model 层只定义结构体和 GORM tag(如
gorm:"primaryKey"),不带任何逻辑
Repository 层怎么写才不踩坑
Repository 不是“把 GORM 封装一层就叫 repository”,它得屏蔽底层差异、统一错误类型、支持测试替换。最常见错误是直接返回 *gorm.DB 或裸 error,导致上层被迫处理 gorm.ErrRecordNotFound 这种框架细节。
- 方法签名统一返回
(T, error)或([]T, error),不暴露*gorm.DB - 把
gorm.ErrRecordNotFound转成自定义 error(如ErrTodoNotFound),上层只关心“没找到”,不关心是不是 GORM - 查询条件用 struct 封装,而不是拼 map 或 string:
type TodoQuery struct { UserID uint; Status string },避免 SQL 注入风险 - 所有方法接收
*gorm.DB作为参数(不是全局变量!),方便测试时传入db.Session(&gorm.Session{DryRun: true})
示例:
func (r *TodoRepository) FindByID(db *gorm.DB, id uint) (*model.Todo, error) {
var todo model.Todo
err := db.First(&todo, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTodoNotFound
}
return &todo, err
}
Service 层怎么安全控制事务
事务起点必须在 service 层,且不能靠 handler 传进来的 *gorm.DB —— 因为 handler 里的 DB 实例没开启事务上下文。正确做法是 service 方法自己调用 db.Transaction(),并在回调里把 DB 实例传给 repository。
- 事务方法名显式带
WithTx后缀(如CreateTodoWithTx),让调用方一眼看出有副作用 - 不要在 repository 层开事务,它只做单次 CRUD;事务边界由 service 定义(比如“扣余额 + 创建订单”必须原子)
- 如果事务中某个 repository 调用失败,
db.Transaction()自动回滚,无需手动tx.Rollback() - 慎用
SavePoint:Fiber 请求生命周期短,嵌套事务易出错,优先拆成多个原子 service 方法
示例:
func (s *TodoService) CreateTodoWithTx(ctx context.Context, todo model.Todo) error {
return s.db.Transaction(func(tx *gorm.DB) error {
if err := s.repo.Create(tx, &todo); err != nil {
return err // 自动回滚
}
return s.repo.UpdateUserStats(tx, todo.UserID)
})
}
模型定义里最容易被忽略的 GORM 配置
光写 type Todo struct { ID uint } 不够,GORM 的零值行为、关联加载、软删除全靠 tag 控制。线上出过太多因 tag 缺失导致的数据错乱。
-
ID uint `gorm:"primaryKey;autoIncrement"`:不加autoIncrement,MySQL 插入时 ID 为 0,后续更新全失效 -
CreatedAt time.Time `gorm:"autoCreateTime"`:用autoCreateTime而非default:CURRENT_TIMESTAMP,避免时区不一致 -
Status string `gorm:"default:'pending'"`:字段设默认值,否则 Go 结构体零值(空字符串)直接入库,覆盖数据库 default - 软删除字段必须叫
DeletedAt且类型为*time.Time,否则Unscoped()失效 - 关联字段加
foreignKey和constraint:比如User gorm.Model `gorm:"foreignKey:UserID"`,不然预加载Preload("User")查不到
复杂点在于:这些 tag 不是写完就完事,得配合 db.Migrator().AutoMigrate() 生效,而且 AutoMigrate 不会删字段、不改类型——上线前必须人工核对 schema 变更。











