GORM 进阶:关联(HasOne/HasMany/Many2Many)与事务
单表 CRUD 只是开始,真实业务几乎都是「用户带资料、文章带标签」这样的关联结构。本章讲 GORM v2 的三种关联映射与事务用法,API 以官方文档 gorm.io 的关联、事务章节为准。
1. 定义三种关联
关联全部用结构体标签声明,外键遵循 XxxID 命名约定:
type Profile struct {
ID uint
UserID uint // 外键,约定为 UserID
Bio string
}
type Tag struct {
ID uint
Name string
}
type Post struct {
ID uint
UserID uint
Title string
Tags []Tag `gorm:"many2many:post_tags;"` // 多对多,迁移时自动建中间表
}
type User struct {
ID uint
Name string
Profile Profile // HasOne:User 拥有一条 Profile
Posts []Post // HasMany:User 拥有多条 Post
}
HasOne 是「一个拥有一个」,HasMany 是「一个拥有多个」,Many2Many 需要中间表;若外键名不符合约定,用 gorm:"foreignKey:..." 显式指定。
2. 预加载 Preload
关联不会在 First 时自动带出,需要显式 Preload,否则拿到的是空值:
var u User
db.Preload("Profile").Preload("Posts").First(&u, 1)
// 嵌套预加载:文章再带出标签
db.Preload("Posts.Tags").First(&u, 1)
列表查询时的 N+1 问题(每行额外查一次)就是靠 Preload 合并查询解决的。
3. 维护关联
除了手动填外键,也可以用关联模式操作关系:
tag := Tag{Name: "gin"}
db.Create(&tag)
db.Model(&u).Association("Tags").Append(&tag) // 追加
db.Model(&u).Association("Tags").Replace(&tags) // 整组替换
db.Model(&u).Association("Tags").Clear() // 清空
4. 事务:要么全成,要么全无
db.Transaction 传入闭包,返回非 nil 错误即整体回滚:
err := db.Transaction(func(tx *gorm.DB) error {
if e := tx.Create(&u).Error; e != nil {
return e // 回滚
}
for i := range u.Posts {
u.Posts[i].UserID = u.ID // 补外键
}
if e := tx.Create(&u.Posts).Error; e != nil {
return e
}
return nil // 提交
})
注意闭包里必须用 tx 而不是外层的 db,否则事务失效。
注意点
AutoMigrate遇到 many2many 标签会自动创建中间表,但中间表结构变化需自行迁移。- 预加载的关系为空不会报错,是零值,判空要看
len()。 - 事务只覆盖数据库:闭包里的副作用(如发邮件、调第三方)失败不会自动回滚。
- 并发写同一行时事务不解决冲突,需要行锁或乐观锁,详见 gorm.io 官方文档。
小结
关联配置记住三步:「标签声明 + Preload 读取 + Association 维护」;事务牢记「闭包内一律用 tx、出错即返回错误」。多对多与预加载是文章、商品类业务最常用的组合,实战章节会大量用到。