关联关系


第六章:关联关系

6.1 关联类型概览

GORM 支持四种主要关联关系:

关系 说明 数据库实现
Belongs To 属于(多对一) 外键在子表
Has One 拥有(一对一) 外键在关联表
Has Many 拥有多个(一对多) 外键在关联表
Many To Many 多对多 中间表

6.2 Belongs To(属于)

场景:用户属于一个公司,多对一关系

type Company struct {
    ID   int
    Name string
}

type User struct {
    ID        int
    Name      string
    CompanyID int      // 外键
    Company   Company  // 关联 Company
}

重写外键

type User struct {
    ID         int
    Name       string
    CorpID     int     // 外键字段
    Corp       Company `gorm:"foreignKey:CorpID"`  // 指定外键
}

重写引用

type Company struct {
    Code string `gorm:"primaryKey"`  // 非 ID 主键
    Name string
}

type User struct {
    ID          int
    Name        string
    CompanyCode string  // 外键对应 Code
    Company     Company `gorm:"references:Code"`  // 指定引用字段
}

6.3 Has One(拥有一个)

场景:用户拥有一张信用卡,一对一关系

type User struct {
    ID          uint
    Name        string
    CreditCard  CreditCard  // 拥有信用卡
}

type CreditCard struct {
    ID     uint
    Number string
    UserID uint   // 外键
}

指定外键

type User struct {
    ID          uint
    Name        string
    CreditCard  CreditCard `gorm:"foreignKey:UserRefer"`  // 指定外键字段名
}

type CreditCard struct {
    ID         uint
    Number     string
    UserRefer  uint   // 外键字段名
}

指定引用

type User struct {
    ID         string `gorm:"primaryKey"`
    Name       string
    CreditCard CreditCard `gorm:"foreignKey:UserID;references:ID"`
}

6.4 Has Many(拥有多个)

场景:用户拥有多篇文章,一对多关系

type User struct {
    ID       uint
    Name     string
    Articles []Article  // 拥有多篇文章
}

type Article struct {
    ID       uint
    Title    string
    UserID   uint   // 外键
}

指定外键和引用

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

type Article struct {
    ID       uint
    Title    string
    AuthorID uint   // 外键对应 User.ID
}

自引用

type Category struct {
    ID       uint
    Name     string
    ParentID *uint
    Children []Category `gorm:"foreignKey:ParentID"`
}

6.5 Many To Many(多对多)

场景:文章有多个标签,标签属于多篇文章

type Article struct {
    ID    uint
    Title string
    Tags  []Tag `gorm:"many2many:article_tags;"`  // 指定连接表
}

type Tag struct {
    ID       uint
    Name     string
    Articles []Article `gorm:"many2many:article_tags;"`
}

自定义连接表

type Article struct {
    ID    uint
    Title string
    Tags  []Tag `gorm:"many2many:article_tags;joinForeignKey:ArticleID;joinReferences:TagID"`
}

type Tag struct {
    ID       uint
    Name     string
    Articles []Article `gorm:"many2many:article_tags;joinForeignKey:TagID;joinReferences:ArticleID"`
}

// 生成的连接表:article_tags
// article_id | tag_id

带额外字段的连接表

// 定义完整的连接表模型
type ArticleTag struct {
    ArticleID uint      `gorm:"primaryKey"`
    TagID     uint      `gorm:"primaryKey"`
    CreatedAt time.Time // 额外字段
}

type Article struct {
    ID    uint
    Title string
    Tags  []Tag `gorm:"many2many:article_tags;"`
}

type Tag struct {
    ID       uint
    Name     string
    Articles []Article `gorm:"many2many:article_tags;"`
}

// 迁移时需要单独创建连接表
db.AutoMigrate(&Article{}, &Tag{}, &ArticleTag{})

6.6 预加载(Preloading)

基础预加载

// 预加载关联数据
db.Preload("Company").Find(&users)
// SELECT * FROM users
// SELECT * FROM companies WHERE id IN (1,2,3,...)

// 预加载嵌套关联
db.Preload("Orders.Items").Find(&users)
// users -> orders -> items

带条件的预加载

// 预加载时添加条件
db.Preload("Orders", "status = ?", 1).Find(&users)
// 只加载 status=1 的订单

// 使用函数
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
    return db.Order("orders.amount DESC")
}).Find(&users)

多个预加载

db.Preload("Orders").
    Preload("CreditCard").
    Preload("Company").
    Find(&users)

Joins 预加载

// 使用 JOIN 替代单独的查询(适合数据量小的情况)
db.Joins("Company").Find(&users)
// SELECT users.*, companies.* FROM users LEFT JOIN companies ...

// 带条件的 Joins
db.Joins("Company", db.Where(&Company{Status: 1})).Find(&users)

预加载全部

// 预加载所有关联(慎用,可能导致性能问题)
db.Preload(clause.Associations).Find(&users)

6.7 关联操作

查找关联

// 加载用户及其文章
db.Model(&user).Association("Articles").Find(&articles)

// 带条件
db.Model(&user).Where("status = ?", 1).Association("Articles").Find(&articles)

添加关联

// 为用户添加文章
db.Model(&user).Association("Articles").Append(&articles)

// 添加单个
db.Model(&user).Association("Articles").Append(&article)

// 使用 ID
db.Model(&user).Association("Articles").Append(&Article{ID: 1})

替换关联

// 替换用户的所有文章
db.Model(&user).Association("Articles").Replace(&newArticles)

删除关联

// 删除关联关系(不删除记录,只删除外键)
db.Model(&user).Association("Articles").Delete(&articles)

// 清空所有关联
db.Model(&user).Association("Articles").Clear()

计数

// 获取关联数量
count := db.Model(&user).Association("Articles").Count()

6.8 关联创建与更新

自动创建关联

user := User{
    Name: "张三",
    CreditCard: CreditCard{Number: "4111111111111111"},
    Articles: []Article{
        {Title: "文章1"},
        {Title: "文章2"},
    },
}
db.Create(&user)
// 自动创建 user、credit_card、articles 记录

跳过关联创建

db.Omit("CreditCard").Create(&user)  // 不创建信用卡
db.Omit("Articles").Create(&user)    // 不创建文章

自动更新关联

user.Articles[0].Title = "新标题"
db.Save(&user)  // 保存用户并更新关联文章

6.9 级联删除

物理删除级联

// 使用 SELECT 删除(先加载关联再删除)
db.Select("Orders").Delete(&user)
// DELETE FROM orders WHERE user_id = ?
// DELETE FROM users WHERE id = ?

// 删除多个关联
db.Select("Orders", "CreditCard").Delete(&user)

标签控制级联

type User struct {
    ID       uint
    Name     string
    Articles []Article `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
    // OnDelete: CASCADE, SET NULL, RESTRICT, NO ACTION
    Company  Company   `gorm:"constraint:OnUpdate:CASCADE,OnDelete:RESTRICT;"`
}

6.10 完整实战示例

package main

import (
    "fmt"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

// 公司
type Company struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string
    Users []User // Has Many
}

// 用户
type User struct {
    ID          uint        `gorm:"primaryKey"`
    Name        string
    CompanyID   uint        // Belongs To
    Company     Company     // Belongs To
    CreditCard  CreditCard  // Has One
    Articles    []Article   // Has Many
    Tags        []Tag       `gorm:"many2many:user_tags;"` // Many To Many
}

// 信用卡
type CreditCard struct {
    ID     uint   `gorm:"primaryKey"`
    Number string
    UserID uint   // 外键
}

// 文章
type Article struct {
    ID     uint   `gorm:"primaryKey"`
    Title  string
    UserID uint   // 外键
}

// 标签
type Tag struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string
    Users []User `gorm:"many2many:user_tags;"`
}

func main() {
    db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    db.AutoMigrate(&Company{}, &User{}, &CreditCard{}, &Article{}, &Tag{})

    // ===== 创建数据 =====
    
    // 创建公司
    company := Company{Name: "Tech Corp"}
    db.Create(&company)

    // 创建用户(同时创建关联)
    user := User{
        Name:       "张三",
        CompanyID:  company.ID,
        CreditCard: CreditCard{Number: "4111111111111111"},
        Articles: []Article{
            {Title: "GORM 入门"},
            {Title: "Go 语言最佳实践"},
        },
    }
    db.Create(&user)

    // 创建标签并关联
    tags := []Tag{
        {Name: "Go"},
        {Name: "Database"},
    }
    db.Create(&tags)
    db.Model(&user).Association("Tags").Append(&tags)

    // ===== 查询演示 =====
    
    // 1. 预加载所有关联
    var loadedUser User
    db.Preload("Company").
        Preload("CreditCard").
        Preload("Articles").
        Preload("Tags").
        First(&loadedUser, user.ID)
    
    fmt.Printf("用户: %s, 公司: %s, 信用卡: %s, 文章数: %d, 标签: %d\n",
        loadedUser.Name,
        loadedUser.Company.Name,
        loadedUser.CreditCard.Number,
        len(loadedUser.Articles),
        len(loadedUser.Tags))

    // 2. 条件预加载
    var usersWithArticles []User
    db.Preload("Articles", "title LIKE ?", "%GORM%").Find(&usersWithArticles)

    // 3. 嵌套预加载
    var companies []Company
    db.Preload("Users.Articles").Find(&companies)

    // ===== 关联操作 =====
    
    // 添加文章
    db.Model(&loadedUser).Association("Articles").Append(&Article{Title: "新文章"})
    
    // 查询文章数量
    count := db.Model(&loadedUser).Association("Articles").Count()
    fmt.Printf("文章数量: %d\n", count)
    
    // 替换标签
    newTags := []Tag{{Name: "ORM"}, {Name: "SQL"}}
    db.Create(&newTags)
    db.Model(&loadedUser).Association("Tags").Replace(&newTags)
    
    // ===== 级联删除 =====
    
    // 删除用户及其所有文章
    db.Select("Articles").Delete(&loadedUser)
    
    fmt.Println("Done!")
}

6.11 性能优化建议

1. 避免 N+1 问题

// 错误:N+1 查询
var users []User
db.Find(&users)
for _, u := range users {
    db.Model(&u).Association("Articles").Find(&u.Articles)  // N 次查询
}

// 正确:预加载
db.Preload("Articles").Find(&users)  // 2 次查询

2. 控制预加载深度

// 避免深层预加载
db.Preload("A.B.C.D").Find(&x)  // 谨慎使用

// 分批加载
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
    return db.Order("created_at DESC").Limit(10)  // 限制数量
}).Find(&users)

3. 使用 Joins 小表

// 关联表数据量小时用 Joins
db.Joins("Company").Find(&users)  // 1 次查询

// 关联表数据量大时用 Preload
db.Preload("Orders").Find(&users)  // 2 次查询,但更高效

6.12 练习题

  1. 设计一个电商系统模型:用户-订单-商品-分类(包含所有四种关系)
  2. 实现一个递归分类查询(支持无限层级)
  3. 优化一个存在 N+1 问题的查询场景

6.13 小结

本章详细讲解了 GORM 的四种关联关系及预加载机制。正确使用关联可以大大简化复杂数据操作,但要注意性能问题,合理使用预加载避免 N+1 查询。


本文代码地址:https://github.com/LittleMoreInteresting/gorm_study

欢迎关注公众号,一起学习进步!

如有疑问关注公众号给我留言
wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo