前言
在使用 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 的关联关系功能丰富且灵活,掌握好以下几点可以帮助你更高效地进行开发:
- 理解关系方向:Has One 和 Belongs To 是同一关系的不同视角
- 合理使用预加载 :根据场景选择
Preload或Joins,避免 N+1 问题 - 善用 Association 模式:方便地操作关联数据,无需手动管理外键
- 注意自引用场景:树形结构在业务中很常见,GORM 支持得很好
希望这篇博客能帮助你更好地理解和使用 GORM 的关联关系。如果你有任何问题或建议,欢迎在评论区留言讨论!