关联关系
第六章:关联关系
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 练习题
- 设计一个电商系统模型:用户-订单-商品-分类(包含所有四种关系)
- 实现一个递归分类查询(支持无限层级)
- 优化一个存在 N+1 问题的查询场景
6.13 小结
本章详细讲解了 GORM 的四种关联关系及预加载机制。正确使用关联可以大大简化复杂数据操作,但要注意性能问题,合理使用预加载避免 N+1 查询。
本文代码地址:https://github.com/LittleMoreInteresting/gorm_study
欢迎关注公众号,一起学习进步!