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 的关联关系。如果你有任何问题或建议,欢迎在评论区留言讨论!

相关推荐
草莓熊Lotso2 小时前
Linux C++ 高并发编程:从原理到手撕,线程池全链路深度解析
linux·运维·服务器·开发语言·数据库·c++·mysql
大龄码农-涵哥2 小时前
MySQL SQL调优详解:explain执行计划、索引失效、慢查询优化一条龙
数据库·sql·mysql
阿丰资源2 小时前
基于SpringBoot+MySQL+Maven+Vue的旅游网站的设计与实现(源码+数据库+文档一键运行)
数据库·spring boot·mysql
派大星酷2 小时前
AOP 完整精讲:原理、核心概念、五种通知、切点语法、自定义注解实战
java·mysql·spring
健康平安的活着2 小时前
mysql中不同时间类型(date/datetime/timestamp)的查询案例
数据库·mysql
健康平安的活着5 小时前
mysql中left join 不一定比 in效率高案例
数据库·mysql
IT摆渡者11 小时前
MySQL性能巡检脚本分析报告
数据库·mysql
Bert.Cai15 小时前
MySQL LPAD()函数详解
数据库·mysql
norq juox17 小时前
MySQL 导出数据
数据库·mysql·adb