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、出错即返回错误」。多对多与预加载是文章、商品类业务最常用的组合,实战章节会大量用到。

笔记加载中…