GORM不适合从零手写SQL者直接上手,踩坑集中于模型定义、迁移逻辑和预加载时机三处:AutoMigrate只增不删不改,gorm.Model与自定义主键勿混用,Preload需注意软删除、N+1及事务透传问题。

直接说结论:GORM 不适合“从零手写 SQL 的人”跳过来就上手,它有一套隐式约定和行为边界,踩坑多发生在模型定义、迁移逻辑和预加载时机这三处。
AutoMigrate 只增不删不改,别当生产环境的表结构管理工具
很多人把 db.AutoMigrate(&User{}) 当成数据库版本控制,结果上线后发现字段类型没变、旧列删不掉、索引加不上。它本质是“安全补丁式同步”:只检查当前结构体有没有对应列,没有就加,有就跳过;不会修改已有列类型(哪怕你改了 gorm:"size:32"),也不会删除废弃字段。
- 开发阶段可用,但上线前必须导出 SQL 对比差异,尤其注意
NOT NULL、DEFAULT和索引变更 - 生产环境推荐用独立迁移工具(如
golang-migrate),或手动执行 DDL - 如果非要用 AutoMigrate,加个
db.Migrator().DropColumn(&User{}, "old_field")手动清理,但得先确认该字段无数据
模型字段标签里,gorm:"primaryKey" 和 gorm.Model 别混用
新手常把 gorm.Model 嵌入结构体,又手动加 ID uint `gorm:"primaryKey"`,结果主键重复定义,建表失败或插入报错 ERROR: duplicate key value violates unique constraint。
-
gorm.Model已包含ID uint、CreatedAt、UpdatedAt、DeletedAt四个字段,且默认ID是主键 - 若自定义主键(比如用
uuid string),就别嵌入gorm.Model,改用type User struct { UUID string `gorm:"primaryKey;type:char(36)"` } - 时间字段想关掉自动更新?加
UpdatedAt time.Time `gorm:"autoUpdateTime:false"`,不是删掉字段
Preload 关联查询时,N+1 问题不是加了就解决
db.Preload("Orders").Find(&users) 看似解决了 N+1,但如果 Orders 关联的是软删除表(带 DeletedAt),默认会把已软删除的记录也查出来——这不是 bug,是 GORM 的默认行为。
- 显式过滤软删除:用
db.Preload("Orders", func(db *gorm.DB) *gorm.DB { return db.Unscoped().Where("deleted_at IS NULL") }) - 批量预加载多个关联时,别链式写
Preload("A").Preload("B").Preload("C"),GORM 会发 3 次 JOIN 查询;改用Joins("A").Joins("B").Joins("C")合并在一条 SQL 里(注意去重逻辑要自己处理) - 关联字段为空时,
Preload返回空 slice 而不是nil,判断要用len(user.Orders) == 0,不是user.Orders == nil
事务里嵌套调用函数,db.Transaction 不会自动透传
写了个 func CreateUser(db *gorm.DB, u *User) error,里面用了 db.Create(u);再在外面套事务:db.Transaction(func(tx *gorm.DB) error { return CreateUser(db, u) })——结果事务根本没生效,因为传进去的是原始 db,不是事务句柄 tx。
- 所有内部函数必须接收
*gorm.DB参数,并统一用它操作,不能硬编码全局DB - 想复用逻辑又不想改签名?用
db.Session(&gorm.Session{NewDB: true})创建新会话,但注意它不继承事务上下文 - 调试事务是否生效:开日志
db.Debug(),看 SQL 是否在BEGIN和COMMIT/ROLLBACK之间
最易被忽略的其实是字段零值处理:Go 的 int 默认是 0,string 是 "",但数据库里它们可能该是 NULL。用指针类型(*string)或 sql.NullString 是显式选择,不是可选项。











