关联关系
GORM 通过 Struct Tag 和关联方法完整支持四种关系映射:Belongs To、Has One、Has Many、Many To Many,以及多态关联。
关联类型总览
| 关系 | 示例 | Go 模型体现 |
|---|---|---|
| Belongs To | 用户 → 公司 | User 含 CompanyID + Company Company |
| Has One | 用户 → 身份证 | User 含 IDCard IDCard |
| Has Many | 用户 → 订单 | User 含 Orders []Order |
| Many To Many | 订单 ↔ 商品 | Order 含 Products []Product(中间表) |
| 多态 | 图片可以属于多种模型 | Picture 含 OwnerType + OwnerID |
Belongs To — 属于
“用户属于某公司”:在 users 表中存 company_id 外键。
type Company struct {
ID uint `gorm:"primaryKey"`
Name string
}
type User struct {
gorm.Model
Name string
CompanyID int // 外键字段
Company Company // 关联对象
}
// 创建
db.Create(&User{
Name: "张三",
Company: Company{Name: "某科技公司"},
})
// INSERT INTO companies (name) VALUES ('某科技公司')
// INSERT INTO users (name, company_id) VALUES ('张三', 1)
// 查询 + 预加载
var user User
db.Preload("Company").First(&user, 1)
// SELECT * FROM users WHERE id = 1
// SELECT * FROM companies WHERE id = user.company_id自定义外键
type User struct {
gorm.Model
Name string
CompanyRef int // 自定义外键字段名
Company Company `gorm:"foreignKey:CompanyRef"` // 指定外键
// 或完整指定:
Company Company `gorm:"foreignKey:CompanyRef;references:ID"` // 指定外键 + 被引用字段
}Has One — 拥有一个
“用户拥有一个身份证”:在 id_cards 表中存 user_id 外键。
type User struct {
gorm.Model
Name string
IDCard IDCard // 关联对象
}
type IDCard struct {
gorm.Model
Number string
UserID uint // 外键字段
}
// 创建(同时创建关联)
db.Create(&User{
Name: "张三",
IDCard: IDCard{Number: "110101199001011234"},
})
// 预加载查询
db.Preload("IDCard").First(&user, 1)自定义外键
type User struct {
gorm.Model
Name string
IDCard IDCard `gorm:"foreignKey:OwnerID"` // 指定 IDCard 中的外键字段名
}
type IDCard struct {
gorm.Model
Number string
OwnerID uint // 自定义外键
}Has Many — 拥有多个
“用户拥有多个订单”:在 orders 表中存 user_id 外键。
type User struct {
gorm.Model
Name string
Orders []Order // 关联
}
type Order struct {
gorm.Model
Amount float64
UserID uint // 外键
}
// 创建
db.Create(&User{
Name: "张三",
Orders: []Order{
{Amount: 100},
{Amount: 200},
},
})
// 预加载
db.Preload("Orders").First(&user, 1)
// 条件预加载
db.Preload("Orders", "amount > ?", 150).First(&user, 1)
// 预加载 + 排序
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Order("amount DESC").Limit(5)
}).First(&user, 1)Many To Many — 多对多
“用户可以有多个角色,角色也可以分配给多个用户”:需要中间表 user_roles。
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;"`
}
// 自动创建中间表 user_roles:
// CREATE TABLE user_roles (
// user_id bigint NOT NULL,
// role_id bigint NOT NULL,
// PRIMARY KEY (user_id, role_id)
// )多对多 CRUD
// 创建(自动写入中间表)
db.Create(&User{
Name: "张三",
Roles: []Role{
{Name: "管理员"},
{Name: "编辑"},
},
})
// 添加关联(已有记录)
db.Model(&user).Association("Roles").Append(&role)
// 替换关联
db.Model(&user).Association("Roles").Replace([]Role{role1, role2})
// 删除关联(只删中间表记录,不删 Role 本身)
db.Model(&user).Association("Roles").Delete(&role)
// 清空所有关联
db.Model(&user).Association("Roles").Clear()
// 统计关联数
count := db.Model(&user).Association("Roles").Count()自定义中间表
type User struct {
gorm.Model
Name string
Roles []Role `gorm:"many2many:user_roles;foreignKey:ID;joinForeignKey:UserID;references:ID;joinReferences:RoleID"`
}| 标签 | 说明 |
|---|---|
many2many:table |
中间表名 |
foreignKey:Field |
当前模型在中间表的外键字段名 |
joinForeignKey:Field |
当前模型在中间表的外键列名 |
references:Field |
被引用的字段(默认主键) |
joinReferences:Field |
关联模型在中间表的外键列名 |
中间表附加字段
当中间表有额外字段时,需显式定义中间表模型:
type UserRole struct {
UserID uint `gorm:"primaryKey"`
RoleID uint `gorm:"primaryKey"`
AssignedAt time.Time
ExpiresAt *time.Time
}
// 查询时加载中间表数据
var userRoles []UserRole
db.Where("user_id = ?", userID).Find(&userRoles)关联模式(Association Mode)
关联模式用于操作关联记录,不会影响主记录本身:
user := User{ID: 1}
// 添加关联
db.Model(&user).Association("Orders").Append([]Order{
{Amount: 500},
{Amount: 600},
})
// 替换所有关联
db.Model(&user).Association("Orders").Replace([]Order{{Amount: 999}})
// 删除指定关联
db.Model(&user).Association("Orders").Delete(&order1)
// 清空所有关联(中间表删除记录,外键表 SET NULL 或删除——取决于约束)
db.Model(&user).Association("Orders").Clear()
// 统计关联数
count := db.Model(&user).Association("Orders").Count()
// 查找关联
var orders []Order
db.Model(&user).Association("Orders").Find(&orders)Preload 完整指南
Preload 是 GORM 中最重要的关联查询方法,用于在查询主记录时一并加载关联数据。
基本用法
// 加载单个关联
db.Preload("Orders").Find(&users)
// 加载嵌套关联
db.Preload("Orders.OrderItems").Find(&users)
// 加载多个关联
db.Preload("Orders").Preload("IDCard").Find(&users)
// 条件预加载
db.Preload("Orders", "status = ?", "paid").Find(&users)
// 自定义预加载
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "paid").Order("created_at DESC").Limit(10)
}).Find(&users)Preload vs Joins
| 方法 | 查询次数 | 返回数据 | 适用场景 |
|---|---|---|---|
Preload |
2 次(主表 + 关联表) | 主记录和关联记录都完整 | 需要结构化数据(填充到嵌套结构体) |
Joins |
1 次(JOIN 单条 SQL) | 只有主记录(关联记录需手动填充) | 用小表条件过滤主记录 |
Joins + Preload |
与 Preload 相同 | 与 Preload 相同 | 需用关联表过滤但也要完整数据 |
// Preload:2 条 SQL
db.Preload("Orders").Find(&users)
// SELECT * FROM users
// SELECT * FROM orders WHERE user_id IN (1,2,3,...)
// Joins:1 条 SQL(但没有填充 orders)
db.Joins("Orders").Find(&users)
// SELECT users.* FROM users LEFT JOIN orders ON ...
// Joins + Preload:1 条 JOIN + 填充(但 Preload 还会发第 2 条)
db.Joins("Company").Preload("Orders").Find(&users)预加载全部关联
db.Preload(clause.Associations).Find(&users) // 所有一级关联关联操作的事务性
// GORM 默认将关联的创建/更新包裹在单独的事务中
db.Create(&User{
Name: "张三",
Orders: []Order{{Amount: 100}},
})
// 如果要跳过关联保存
db.Session(&gorm.Session{FullSaveAssociations: false}).Create(&user)
// 或者手动控制
db.Omit("Orders").Create(&user) // 不保存 Orders💡 创建、更新时默认保存关联。如果不希望自动保存,用
Select/Omit排除关联字段。
关联删除行为
// 删除用户时,关联的订单如何处理?
type User struct {
gorm.Model
Orders []Order `gorm:"constraint:OnDelete:CASCADE"` // 级联删除
}
// 删除用户 → 自动删除其所有订单
type User struct {
gorm.Model
Orders []Order `gorm:"constraint:OnDelete:SET NULL"` // 设为 NULL
}
// 删除用户 → 订单的 user_id 设为 NULL| 约束选项 | 行为 |
|---|---|
OnDelete:CASCADE |
级联删除关联记录 |
OnDelete:SET NULL |
关联记录的外键设为 NULL |
OnDelete:RESTRICT |
有关联记录时阻止删除 |
OnUpdate:CASCADE |
主键变更时同步更新外键 |
🚨 陷阱:
gorm.Config.DisableForeignKeyConstraintWhenMigrating: true(许多项目的常见配置)禁用物理外键。此时constraint:OnDelete:CASCADE只影响 GORM 的迁移生成,运行时不会生效。需要自行处理级联逻辑或使用数据库 Migration 手动创建外键。
多态关联
一个模型可以属于多个不同的模型类型:
// 图片可以属于用户、商品、文章等
type Picture struct {
gorm.Model
URL string
OwnerType string // "users" / "products" / "articles"
OwnerID uint
}
type User struct {
gorm.Model
Name string
Pictures []Picture `gorm:"polymorphic:Owner;"`
}
type Product struct {
gorm.Model
Title string
Pictures []Picture `gorm:"polymorphic:Owner;"`
}
// Picture 表中:
// | id | url | owner_type | owner_id |
// | 1 | /a.jpg | users | 1 |
// | 2 | /b.jpg | products | 3 |多态关联不适合所有场景——当关联类型数有限时,考虑使用多个单独的外键字段替代。
常见陷阱汇总
| 陷阱 | 说明 |
|---|---|
| 🚨 N+1 查询 | 循环中使用关联而不预加载(使用 Preload 解决) |
| 🚨 外键约束失效 | DisableForeignKeyConstraintWhenMigrating: true 后不创建物理外键 |
| 🚨 Preload 无效字段名 | Preload 参数是结构体字段名(Go 侧),不是数据库列名 |
| 🚨 多对多 Append 不查重 | Append 不检查是否已存在关联,重复追加导致中间表重复行 |
| 🚨 Association + 空主键 | 调用 Association 时主记录必须有主键值,否则操作无效 |
| 🚨 关联自动保存 | Create/Update 时默认保存关联,可能创建你不想要的新记录 |
| 🚨 Has Many 空切片 | Preload("Orders") 时若订单不存在,Orders 为 nil(不是空切片) |