5月28日 01:11

GORM 中的关联关系有哪些类型?

GORM 支持四种核心关联关系:Belongs To(属于)、Has One(有一个)、Has Many(有多个)、Many To Many(多对多),另外还支持多态关联。面试中最常考的是四种基本关系的区别与预加载机制。

一张表记住四种关系

关系类型方向外键位置典型场景
Belongs To子→父子模型中用户属于某个部门
Has One父→子关联模型中用户有一张身份证
Has Many父→子(多个)关联模型中用户有多个订单
Many To Many双向多对多中间表中用户拥有多个角色

核心记忆:Belongs To 外键在自己身上,其余三种外键都不在自己身上。

Belongs To(属于)

一个模型"属于"另一个模型,外键定义在当前模型中。这是唯一一种外键在声明方的关联类型。

go
type Department struct { ID uint Name string } type User struct { gorm.Model Name string DepartmentID uint // 外键在 User 中 Department Department `gorm:"foreignKey:DepartmentID"` } // 查询时预加载关联 var user User db.Preload("Department").First(&user, 1)

Has One(有一个)

一个模型拥有另一个模型,外键在关联模型中。与 Belongs To 的区别在于视角:Has One 从"拥有方"定义,外键在对方。

go
type CreditCard struct { gorm.Model Number string UserID uint // 外键在 CreditCard 中 } type User struct { gorm.Model Name string CreditCard CreditCard } db.Preload("CreditCard").First(&user, 1)

Has Many(有多个)

一个模型拥有多个关联模型,是最常用的关联类型。外键在关联模型中,查询结果为切片。

go
type Order struct { gorm.Model UserID uint Amount float64 } type User struct { gorm.Model Name string Orders []Order } // 基础预加载 db.Preload("Orders").First(&user, 1) // 条件预加载:只加载金额大于 100 的订单 db.Preload("Orders", "amount > ?", 100).First(&user, 1)

Many To Many(多对多)

两个模型互为多对多关系,通过中间表实现。GORM 自动创建中间表,默认命名规则为 模型1_模型2

go
type User struct { gorm.Model Name string Roles []Role `gorm:"many2many:user_roles;"` } type Role struct { gorm.Model Name string Users []User `gorm:"many2many:user_roles;"` } // 预加载 db.Preload("Roles").First(&user, 1) // Association 操作 db.Model(&user).Association("Roles").Append(&Role{Name: "Admin"}) db.Model(&user).Association("Roles").Delete(&Role{Name: "Admin"}) db.Model(&user).Association("Roles").Replace([]Role{role1, role2}) db.Model(&user).Association("Roles").Clear() count := db.Model(&user).Association("Roles").Count()

多态关联

GORM 支持多态的 Has One 和 Has Many,即一个模型可以被多种其他模型关联。

go
type Comment struct { gorm.Model Content string CommentableID uint CommentableType string } type Post struct { gorm.Model Title string Comments []Comment `gorm:"polymorphic:Commentable;"` } type Video struct { gorm.Model Name string Comments []Comment `gorm:"polymorphic:Commentable;"` }

多态关联通过 CommentableID + CommentableType 两个字段实现,CommentableType 存储关联模型的表名。

自定义关联配置

自定义外键

go
type User struct { gorm.Model CreditCards []CreditCard `gorm:"foreignKey:UserRefer"` } type CreditCard struct { gorm.Model Number string UserRefer uint }

自定义引用键

go
type User struct { gorm.Model Name string `gorm:"index"` CreditCard CreditCard `gorm:"foreignKey:UserName;references:Name"` } type CreditCard struct { gorm.Model Number string UserName string }

自定义中间表字段

go
type User struct { gorm.Model Roles []Role `gorm:"many2many:user_roles;joinForeignKey:UserID;joinReferences:RoleID"` }

预加载:解决 N+1 查询问题

N+1 问题是关联查询最常见的性能陷阱:查询 N 条主记录后,每条记录再发一次查询加载关联数据,共 N+1 次查询。预加载将关联数据一次性查出。

go
// 基础预加载 db.Preload("Orders").Find(&users) // 嵌套预加载 db.Preload("Orders.Items").Find(&users) // 条件预加载 db.Preload("Orders", "status = ?", "completed").Find(&users) // 多关联预加载 db.Preload("Orders").Preload("CreditCard").Find(&users) // JoinsPreload(使用 JOIN 代替子查询,适合单条记录) db.JoinsPreload("Orders").First(&user, 1)

级联删除与外键约束

GORM 默认删除主记录不会删除关联记录,需要显式配置级联行为:

go
type User struct { gorm.Model Name string Orders []Order `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` } type Order struct { gorm.Model UserID uint Amount float64 }

也可以在删除时通过 Select 显式删除关联记录:

go
// 删除用户及其所有订单 db.Select("Orders").Delete(&user) // 删除用户及其订单和信用卡 db.Select("Orders", "CreditCard").Delete(&user)

面试常见追问

Belongs To 和 Has One 有什么区别? 本质都是一对一关系,区别在于外键位置:Belongs To 的外键在声明方,Has One 的外键在关联方。选择标准是语义:如果 A "属于" B,用 Belongs To;如果 A "拥有" B,用 Has One。

多对多中间表可以加额外字段吗? 可以。需要自定义中间表模型,将 many2many 标签改为手动定义的关联模型,中间表可以有额外字段如 CreatedAtRole 等。

如何避免 N+1 查询? 使用 Preload 预加载关联数据,或在需要单条记录时使用 JoinsPreload。也可以用 Join 手动编写 JOIN 查询。

标签:Gorm