Skip to content
Go
关联关系

关联关系

GORM 通过 Struct Tag 和关联方法完整支持四种关系映射:Belongs To、Has One、Has Many、Many To Many,以及多态关联。


关联类型总览

关系 示例 Go 模型体现
Belongs To 用户 → 公司 UserCompanyID + Company Company
Has One 用户 → 身份证 UserIDCard IDCard
Has Many 用户 → 订单 UserOrders []Order
Many To Many 订单 ↔ 商品 OrderProducts []Product(中间表)
多态 图片可以属于多种模型 PictureOwnerType + 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(不是空切片)