GORM 关联关系完全指南:从入门到精通

前言

在使用 GORM 进行数据库开发时,关联关系的处理是绕不开的核心话题。无论是一对一、一对多还是多对多关系,GORM 都提供了优雅的解决方案。本文将深入浅出地介绍 GORM 中各种关联关系的使用方法,助你快速上手并熟练运用。

1. 关联关系概述

GORM 通过结构体标签定义数据库表之间的关联关系,支持四种标准关系类型:

关系类型 Tag 标识 说明
Belongs To belongs to 「从属」关系,子表持有父表的外键
Has One has one 「拥有」关系,一对一
Has Many has many 「拥有」关系,一对多
Many To Many many2many 多对多,需要中间表

💡 小贴士:GORM 还支持多态关联(Polymorphism),但多对多场景不支持。

2. 一对一(Has One / Belongs To)

2.1 Has One(拥有关系)

Has One 表示一个模型拥有另一个模型的一个实例。例如,每个用户只能有一张信用卡:

go

复制代码
// User 有一张 CreditCard,UserID 是外键
type User struct {
    gorm.Model
    CreditCard CreditCard
}

type CreditCard struct {
    gorm.Model
    Number string
    UserID uint   // 外键,默认规则:拥有者类型名 + 主键字段名
}

2.2 Belongs To(从属关系)

Belongs To 与 Has One 实际上是双向描述的。从 CreditCard 角度看,它属于一个 User:

go

复制代码
type CreditCard struct {
    gorm.Model
    Number string
    UserID uint
    User   User `gorm:"foreignKey:UserID"`   // 指定外键
}

2.3 重写外键与引用

如果不想使用默认的外键字段名,可以用 foreignKey 标签指定:

go

复制代码
type User struct {
    gorm.Model
    Name       string     `gorm:"index"`
    CreditCard CreditCard `gorm:"foreignKey:UserName;references:Name"`
    // 使用 UserName 作为外键,参考 User 的 Name 字段
}

type CreditCard struct {
    gorm.Model
    Number   string
    UserName string   // 自定义外键字段
}
// CreditCard 表的 UserName 字段 → 指向 → User 表的 Name 字段

参数说明:

  • foreignKey:指定当前"被拥有"模型中用于存储关联的外键字段
  • references:指定"拥有者"模型中被引用的字段(默认为主键)

2.4 外键约束

通过 constraint 标签配置级联操作:

go

复制代码
type User struct {
    gorm.Model
    CreditCard CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}
  • OnUpdate:CASCADE:更新主表时,外键级联更新
  • OnDelete:SET NULL:删除主表记录时,外键设置为 NULL

2.5 自引用一对一

可以用 Has One 实现自引用,例如用户的管理者关系:

go

复制代码
type User struct {
    gorm.Model
    Name      string
    ManagerID *uint   // 外键指向自身的主键
    Manager   *User   // 关联的用户对象
}

3. 一对多(Has Many)

Has Many 表示一个模型拥有另一个模型的多个实例。例如,一个用户可以有多张信用卡:

3.1 声明

go

复制代码
type User struct {
    gorm.Model
    CreditCards []CreditCard   // 切片类型,表示一对多
}

type CreditCard struct {
    gorm.Model
    Number string
    UserID uint   // 外键
}

3.2 重写外键与引用

go

复制代码
type User struct {
    gorm.Model
    MemberNumber string
    CreditCards  []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
    // 使用 UserNumber 作为外键,参考 MemberNumber 字段
}

type CreditCard struct {
    gorm.Model
    Number     string
    UserNumber string   // 自定义外键
}

3.3 自引用一对多(树形结构)

常用于组织架构、菜单树等场景:

go

复制代码
type User struct {
    gorm.Model
    Name      string
    ManagerID *uint   // 上级用户ID
    Team      []User  `gorm:"foreignKey:ManagerID"`   // 下属列表
}

3.4 外键约束

go

复制代码
type User struct {
    gorm.Model
    CreditCards []CreditCard `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
}

4. 多对多(Many To Many)

4.1 声明

多对多关联需要一张中间表(连接表)。例如,用户可以拥有多个语言,语言也可以被多个用户使用:

go

复制代码
type User struct {
    gorm.Model
    Languages []Language `gorm:"many2many:user_languages;"`
}

type Language struct {
    gorm.Model
    Name  string
    Users []User `gorm:"many2many:user_languages;"`
}
  • many2many:user_languages:指定中间表名为 user_languages
  • 需要在两端都定义 many2many 标签

4.2 自定义中间表

可以通过定义中间模型来扩展额外字段:

go

复制代码
// 中间模型
type UserLanguage struct {
    UserID     uint
    LanguageID uint
    Level      string   // 额外字段:掌握程度
    CreatedAt  time.Time
}

type User struct {
    gorm.Model
    Languages []Language `gorm:"many2many:user_languages"`
}

type Language struct {
    gorm.Model
    Name string
}

5. 多态关联(Polymorphism)

多态允许一个模型属于多个不同类型的模型。例如,Cat 和 Dog 都可以拥有 Toy:

go

复制代码
type Cat struct {
    ID   int
    Name string
    Toy []Toy `gorm:"polymorphic:Owner;"`
}

type Dog struct {
    ID   int
    Name string
    Toy []Toy `gorm:"polymorphic:Owner;"`
}

type Toy struct {
    ID        int
    Name      string
    OwnerID   int
    OwnerType string   // 存储拥有者类型(Cat 或 Dog)
}

⚠️ 注意:多对多(Many2Many)明确不支持多态关联。

6. 预加载(Eager Loading)

预加载用于在查询主模型时,同时加载关联模型的数据,有效避免 N+1 查询问题。

6.1 基础预加载(Preload)

go

复制代码
// 预加载用户的信用卡
var users []User
db.Preload("CreditCards").Find(&users)

// 查询单个用户时预加载
var user User
db.Preload("CreditCards").First(&user, 1)

执行原理:GORM 会先查询主表,再分别查询关联表。

6.2 条件预加载

go

复制代码
// 只预加载金额大于 100 的订单
db.Preload("Orders", "amount > ?", 100).Find(&users)

// 使用函数自定义预加载条件
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
    return db.Order("amount DESC").Limit(5)
}).Find(&users)

6.3 嵌套预加载

go

复制代码
// 预加载订单,以及订单中的商品条目
db.Preload("Orders.OrderItems").Find(&users)

// 更深层嵌套
db.Preload("Orders.OrderItems.Product").Find(&users)

// 为嵌套关联添加条件
db.Preload("Orders", "state = ?", "paid").
   Preload("Orders.OrderItems").Find(&users)

6.4 预加载全部关联

go

复制代码
import "gorm.io/gorm/clause"

// 加载模型定义的所有关联
db.Preload(clause.Associations).Find(&users)

⚠️ 注意clause.Associations 不会预加载嵌套关联,需要嵌套预加载时仍需显式指定。

6.5 Joins 预加载(Inner Join)

对于 Has One 和 Belongs To 关系,可以使用 Joins 实现单条 SQL 预加载:

go

复制代码
// 使用 Joins 预加载,一条 SQL 完成
db.Joins("CreditCard").First(&user)

// 带条件的 Joins 预加载
db.Joins("CreditCard", db.Where(&CreditCard{Number: "123456"})).Find(&users)

// 预加载 Belongs To 关系
db.Joins("Company").Find(&users)

对比总结

方法 SQL 执行次数 适用场景
Preload 1 + N 次 一对多、多对多
Joins 1 次 Has One、Belongs To

7. 关联操作(Association Mode)

GORM 提供了 Association 方法,用于操作关联数据。

7.1 查找关联

go

复制代码
// 查找用户的所有信用卡
db.Model(&user).Association("CreditCards").Find(&creditCards)

7.2 添加/替换关联

go

复制代码
// 添加新的信用卡
db.Model(&user).Association("CreditCards").Append(&newCard1, &newCard2)

// 替换所有关联(删除原有,添加新的)
db.Model(&user).Association("CreditCards").Replace(&newCard)

// 删除关联(不删除数据库记录,仅解除关联关系)
db.Model(&user).Association("CreditCards").Delete(&cardToRemove)

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

7.3 统计关联数量

go

复制代码
// 获取信用卡数量
count := db.Model(&user).Association("CreditCards").Count()

8. 完整示例:综合应用

go

复制代码
// 模型定义
type User struct {
    gorm.Model
    Name         string
    Profile      Profile          // Has One
    Orders       []Order          // Has Many
    Languages    []Language       `gorm:"many2many:user_languages"`  // Many To Many
    ManagerID    *uint
    Subordinates []User           `gorm:"foreignKey:ManagerID"`      // 自引用
}

type Profile struct {
    gorm.Model
    UserID uint
    Phone  string
}

type Order struct {
    gorm.Model
    UserID     uint
    Amount     float64
    OrderItems []OrderItem
}

type OrderItem struct {
    gorm.Model
    OrderID uint
    Product string
    Price   float64
}

type Language struct {
    gorm.Model
    Name  string
    Users []User `gorm:"many2many:user_languages"`
}

// 复杂查询示例
func getUsersWithAllData(db *gorm.DB) ([]User, error) {
    var users []User
    err := db.
        Preload("Profile").                           // 一对一
        Preload("Orders.OrderItems").                 // 一对多嵌套
        Preload("Languages").                         // 多对多
        Preload("Subordinates").                      // 自引用
        Preload("Orders", "amount > ?", 100).         // 条件预加载
        Find(&users).Error
    return users, err
}

总结

GORM 的关联关系功能丰富且灵活,掌握好以下几点可以帮助你更高效地进行开发:

  1. 理解关系方向:Has One 和 Belongs To 是同一关系的不同视角
  2. 合理使用预加载 :根据场景选择 PreloadJoins,避免 N+1 问题
  3. 善用 Association 模式:方便地操作关联数据,无需手动管理外键
  4. 注意自引用场景:树形结构在业务中很常见,GORM 支持得很好

希望这篇博客能帮助你更好地理解和使用 GORM 的关联关系。如果你有任何问题或建议,欢迎在评论区留言讨论!

相关推荐
拾贰_C3 小时前
【Ubuntu | 公共工作站 | mysql 】 MySQL残留物残留数据
linux·mysql·ubuntu
知识汲取者5 小时前
每日一篇高频面试题系列之【MySQL 锁】
数据库·mysql
lolo大魔王6 小时前
Go 后端实战|Gin + GORM V2 + MySQL 企业级 API 项目开发(完整版)
mysql·golang·gin
Hical_W6 小时前
Hical 踩坑实录五部曲(五):Boost.MySQL 协程集成的 5 个坑
数据库·mysql·开源
czlczl200209257 小时前
mysql表复制方案
数据库·mysql
jran-10 小时前
MySQL多表操作 查询&子查询&外键约束
数据库·mysql
看到代码头都是大的10 小时前
CentoOS7安装mysql 8.0.46
mysql
阿坤带你走近大数据13 小时前
DM达梦数据库的介绍
数据库·mysql·oracle·国产信创
数据库小学妹14 小时前
企业级数据库迁移实践:从Oracle到国产数据库的兼容性与实施策略
数据库·mysql·oracle·dba
qq_2975746715 小时前
MySQL核心技术实战系列(第二篇):MySQL核心基础:库与表的增删改查(CRUD)实战
数据库·mysql