一对多关联

一对多是业务中最常见的关联关系。一个用户有多篇文章、一个分类下有多个商品、一个部门有多个员工。

Has Many 关系

用户有多篇文章:

type User struct {
    ID     uint
    Name   string
    Articles []Article
}

type Article struct {
    ID     uint
    UserID uint
    Title  string
}

Article 通过 UserID 外键关联到 User

外键约定

默认外键是 模型名 + ID,即 UserID

自定义外键:

type User struct {
    ID       uint
    Name     string
    Articles []Article `gorm:"foreignKey:AuthorID"`
}

type Article struct {
    ID       uint
    AuthorID uint
    Title    string
}

引用键约定

默认引用主键。自定义引用键:

type User struct {
    ID       uint
    Username string `gorm:"unique"`
    Articles []Article `gorm:"foreignKey:AuthorName;references:Username"`
}

type Article struct {
    ID         uint
    AuthorName string
    Title      string
}

创建关联

创建用户时同时创建文章:

user := User{
    Name: "张三",
    Articles: []Article{
        {Title: "文章1"},
        {Title: "文章2"},
    },
}
db.Create(&user)

三张表都会创建,外键自动设置。

查询关联

var user User
db.Preload("Articles").First(&user, 1)
fmt.Println(len(user.Articles))

条件预加载:

db.Preload("Articles", "status = ?", "published").First(&user, 1)

只加载已发布的文章。

添加关联

给用户添加新文章:

var user User
db.First(&user, 1)

article := Article{Title: "新文章"}
db.Model(&user).Association("Articles").Append(&article)

删除关联

删除用户的某篇文章关联:

db.Model(&user).Association("Articles").Delete(&article)

注意:这只是删除关联(外键置空),不删除文章记录。

要删除记录:

db.Delete(&article)

清空关联

清空用户的所有文章关联:

db.Model(&user).Association("Articles").Clear()

统计关联数量

count := db.Model(&user).Association("Articles").Count()

Belongs To 关系

反向的多对一,文章属于用户:

type Article struct {
    ID     uint
    UserID uint
    User   User
    Title  string
}

查询文章时获取用户:

var article Article
db.Preload("User").First(&article, 1)
fmt.Println(article.User.Name)

双向关联

同时定义 Has Many 和 Belongs To:

type User struct {
    ID       uint
    Name     string
    Articles []Article
}

type Article struct {
    ID     uint
    UserID uint
    User   User
    Title  string
}

这样可以从用户查文章,也可以从文章查用户。

实际案例

分类与商品

type Category struct {
    ID       uint
    Name     string
    Products []Product
}

type Product struct {
    ID         uint
    CategoryID uint
    Category   Category
    Name       string
    Price      float64
}

var category Category
db.Preload("Products").First(&category, 1)

部门与员工

type Department struct {
    ID       uint
    Name     string
    Employees []Employee
}

type Employee struct {
    ID           uint
    DepartmentID uint
    Department   Department
    Name         string
}

订单与订单项

type Order struct {
    ID      uint
    Items   []OrderItem
}

type OrderItem struct {
    ID        uint
    OrderID   uint
    ProductID uint
    Quantity  int
    Price     float64
}

var order Order
db.Preload("Items").First(&order, 1)

级联删除

删除用户时同时删除文章:

type User struct {
    ID       uint
    Articles []Article `gorm:"constraint:OnDelete:CASCADE;"`
}

或者手动删除:

db.Select("Articles").Delete(&user)

外键约束

数据库层面设置约束:

type Article struct {
    ID     uint
    UserID uint `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"`
    Title  string
}

约束选项:

  • OnDelete:CASCADE - 删除用户时删除文章
  • OnDelete:SET NULL - 删除用户时文章外键置空
  • OnDelete:RESTRICT - 有文章时禁止删除用户
  • OnUpdate:CASCADE - 更新用户ID时同步更新文章外键

常见问题

关联为空

忘记预加载:

var user User
db.First(&user, 1)
fmt.Println(user.Articles)

Articles 是空切片。必须预加载:

db.Preload("Articles").First(&user, 1)

N+1 问题

循环中查询关联:

var users []User
db.Find(&users)
for _, user := range users {
    db.Model(&user).Association("Articles").Count()
}

应该预加载:

db.Preload("Articles").Find(&users)

大量关联数据

用户有上万篇文章,预加载会占用大量内存。解决方案:

  • 条件预加载
  • 分页加载关联
  • 延迟加载

小结

一对多是最常用的关联关系。理解 Has Many 和 Belongs To 的双向关系,注意预加载避免 N+1 问题。