gorm关联详讲

GORM 关联(Associations)部分详细讲解

GORM 提供了强大的关联功能,用于管理模型之间的关系,包括 Belongs To、Has One、Has Many 和 Many To Many。这些关联允许你轻松处理数据库中的外键引用、级联操作和查询优化。本讲解基于 GORM 官方文档,旨在帮助你达到项目级实践水平。我们将覆盖关联类型、创建/更新/删除 API、标签、覆盖、外键约束、预加载(Preload)、连接(Joins)以及最佳实践。所有示例假设使用 MySQL,并包括代码和生成的 SQL 解释。

1. 关联类型概述

GORM 支持四种主要关联类型,每种类型通过模型结构体中的字段定义。关联依赖外键(Foreign Key),GORM 会自动推断,但你可以自定义。

  • Belongs To:一个模型“属于”另一个模型,外键通常在当前模型中。示例:User 属于 Company(User 有 CompanyID)。
  • Has One:一个模型“拥有一个”关联记录,外键在关联模型中。示例:User 拥有一个 CreditCard(CreditCard 有 UserID)。
  • Has Many:一个模型“拥有多个”关联记录,外键在关联模型中。示例:User 拥有多个 Orders(Order 有 UserID)。
  • Many To Many:多对多,通过连接表(Join Table)实现。示例:User 和 Language 通过 user_languages 表关联。

最佳实践:在定义关联时,确保外键约束(如 OnDelete: CASCADE)以维护数据完整性。使用迁移(AutoMigrate)自动创建外键。

2. 定义关联模型

关联通过结构体字段定义,使用 gorm 标签自定义。

Belongs To

1
2
3
4
5
6
7
8
9
10
type User struct {
gorm.Model
CompanyID uint // 外键
Company Company `gorm:"foreignKey:CompanyID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"` // 关联标签
}

type Company struct {
gorm.Model
Name string
}
  • 标签解释
    • foreignKey:CompanyID:指定外键字段(默认推断为 CompanyID)。
    • references:ID:引用 Company 的字段(默认 ID)。
    • constraint:OnUpdate:CASCADE,OnDelete:SET NULL:更新时级联,删除时设为 NULL。

Has One

1
2
3
4
5
6
7
8
9
10
type User struct {
gorm.Model
CreditCard CreditCard `gorm:"foreignKey:UserID;references:ID"` // 关联
}

type CreditCard struct {
gorm.Model
UserID uint // 外键
Number string
}

Has Many

1
2
3
4
5
6
7
8
9
10
type User struct {
gorm.Model
Orders []Order `gorm:"foreignKey:UserID;references:ID"` // 切片表示 Has Many
}

type Order struct {
gorm.Model
UserID uint // 外键
Amount float64
}

Many To Many

1
2
3
4
5
6
7
8
9
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;foreignKey:ID;joinForeignKey:UserID;References:ID;joinReferences:LanguageID"` // 通过连接表
}

type Language struct {
gorm.Model
Name string
}
  • 标签解释
    • many2many:user_languages:指定连接表名(默认 user_languages)。
    • joinForeignKey:UserID:连接表中引用 User 的外键。
    • joinReferences:LanguageID:连接表中引用 Language 的外键。

覆盖默认值

  • 默认外键:{OwnerStruct} + {PrimaryKey}(如 UserID)。
  • 默认引用:关联模型的 PrimaryKey(通常 ID)。
  • 覆盖示例:gorm:"foreignKey:OwnerID;references:Code"(如果 Company 的主键是 Code)。

多主键/复合外键

  • 支持复合主键:gorm:"foreignKey:DeptID,BranchID;references:ID,BranchID"

自引用关联

  • Has Many:Children []User gorm:”foreignKey:ParentID””`。
  • Many To Many:Friends []User gorm:”many2many:user_friends””`。

最佳实践:使用标签明确定义外键和约束,避免默认推断导致的错误。在迁移时启用外键约束:db.AutoMigrate(&User{}) 会创建外键。

3. 创建关联(Create APIs)

GORM 在创建记录时自动保存关联(Auto Create),包括插入关联记录和设置外键。使用 Upsert 技术(更新或插入)处理引用。

基本创建

1
2
3
4
5
6
7
8
9
user := User{
Name: "jinzhu",
BillingAddress: Address{Address1: "Billing Address - Address 1"},
ShippingAddress: Address{Address1: "Shipping Address - Address 1"},
Emails: []Email{{Email: "jinzhu@example.com"}, {Email: "jinzhu-2@example.com"}},
Languages: []Language{{Name: "ZH"}, {Name: "EN"}},
}
db.Create(&user)
// 生成 SQL:INSERT INTO addresses ..., users ..., emails ..., languages ..., user_languages ... ON DUPLICATE KEY DO NOTHING
  • APIdb.Create(&model) – 自动保存所有关联。

  • 跳过关联:使用 SelectOmit

    1
    2
    3
    db.Omit("BillingAddress").Create(&user)  // 跳过 BillingAddress
    db.Omit(clause.Associations).Create(&user) // 跳过所有关联
    db.Select("Name", "CreditCard").Create(&user) // 只保存 Name 和 CreditCard
  • Many To Many 特殊Omit("Languages.*") 跳过 Upsert,但保存引用;Omit("Languages") 跳过两者。

选择关联字段

1
2
db.Select("BillingAddress.Address1").Create(&user)  // 只保存 BillingAddress 的 Address1
db.Omit("BillingAddress.Address2").Create(&user) // 排除 Address2

最佳实践:对于大批量创建,使用事务包裹以确保原子性。避免在循环中创建关联,以防性能问题。

4. 查询关联(Query APIs)

GORM 支持懒加载(默认)和预加载(Eager Loading)查询关联。

懒加载

1
2
3
var user User
db.First(&user)
// 访问时加载:db.Model(&user).Association("CreditCard").Find(&user.CreditCard)

预加载(Preload)

避免 N+1 查询问题,使用多条 SELECT 查询加载。

1
2
3
4
5
db.Preload("Orders").Preload("CreditCard").First(&user)
// 嵌套:db.Preload("Orders.Items").Preload("Orders.Items.Product").Find(&users)
// 条件:db.Preload("Orders", "state NOT IN (?)", []string{"cancelled"}).Find(&users)
// 自定义:db.Preload("Orders", func(db *gorm.DB) *gorm.DB { return db.Order("amount DESC") }).Find(&users)
db.Preload("Orders", clause.Eq{Column: "state", Value: "paid"}).Find(&users) // 使用 clause
  • APIdb.Preload("AssociationName") – 支持多个链式调用。
  • 所有关联db.Preload(clause.Associations).Find(&users)

连接查询(Joins)

使用 JOIN 查询关联,通常用于一对一/多,生成单条 SQL。

1
2
3
4
db.Joins("Company").Find(&users)  // SELECT users.*, companies.* FROM users JOIN companies ...
// 条件:db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users)
// 别名:db.Joins("Manager").Joins("Company").Find(&user)
// 扫描到其他结构体:db.Joins("Company").Joins("BillingAddress").Joins("ShippingAddress").Find(&FlatUser{})
  • APIdb.Joins("AssociationName") – 支持条件和别名。
  • 最佳实践:Preload 适合复杂关联(多查询);Joins 适合简单关联(单查询,性能更好)。监控 SQL 日志优化。

5. 更新关联(Update APIs)

GORM 在更新时自动更新关联引用,但需指定模式。

基本更新

1
2
db.Save(&user)  // 保存所有字段,包括关联引用
db.Model(&user).Updates(User{Name: "newname", Company: Company{Name: "newcompany"}}) // 更新非零值

全量更新关联

1
db.Session(&gorm.Session{FullSaveAssociations: true}).Updates(&user)  // 全量更新所有关联

管理关联(Association Mode)

用于精细控制(如 Append、Replace)。

1
2
3
4
5
6
7
8
9
10
// Belongs To / Has One
db.Model(&user).Association("Company").Replace(&Company{Name: "new"}) // 替换
db.Model(&user).Association("Company").Delete(&Company{}) // 删除

// Has Many / Many To Many
db.Model(&user).Association("Languages").Append([]Language{{Name: "DE"}, {Name: "FR"}}) // 添加
db.Model(&user).Association("Languages").Replace([]Language{{Name: "DE"}}) // 替换所有
db.Model(&user).Association("Languages").Delete([]Language{{Name: "ZH"}}) // 删除指定
db.Model(&user).Association("Languages").Clear() // 清空
db.Model(&user).Association("Languages").Count() // 计数
  • API
    • Association("Name").Find(&results):查询关联。
    • Append(values...):添加(Has Many/Many To Many)。
    • Replace(values...):替换所有。
    • Delete(values...):删除指定。
    • Clear():清空。
    • Count() int64:计数。

最佳实践:使用 Association Mode 处理动态关联。结合钩子(Hooks)验证更新。

6. 删除关联(Delete APIs)

删除主记录时,可选择删除关联。

基本删除

1
db.Delete(&user)  // 删除 user,不影响关联(除非约束)

选择删除关联

1
2
db.Select("Orders", "CreditCard").Delete(&user)  // 删除 user 和选定的关联
db.Select(clause.Associations).Delete(&user) // 删除所有关联
  • 约束影响:如果有 OnDelete:CASCADE,自动级联删除。
  • 软删除:关联也支持软删除(如果有 DeletedAt)。

最佳实践:优先使用软删除和约束,避免手动删除导致数据不一致。使用事务包裹多删除操作。

7. 外键约束和覆盖

  • 约束constraint:OnUpdate:CASCADE,OnDelete:RESTRICT – 支持 CASCADE、RESTRICT、SET NULL、SET DEFAULT、NO ACTION。
  • 禁用约束&gorm.Config{DisableForeignKeyConstraintWhenMigrating: true} – 迁移时不创建物理外键,但 GORM 仍逻辑处理。
  • 覆盖:标签覆盖默认外键/引用/连接表。

8. 附加功能和最佳实践

  • 多态关联gorm:"polymorphic:Owner" – 支持多态(如不同模型共享关联)。
  • 多个关联:如 User 有 BillingAddress 和 ShippingAddress(两个 Has One 到 Address)。
  • 自引用 Many To Many:如用户的好友关系。
  • 性能:总是使用 Preload/Joins 避免 N+1。启用 Logger 监控 SQL:&gorm.Config{Logger: logger.Default.LogMode(logger.Info)}
  • 错误处理:检查 db.Error,如 gorm.ErrInvalidField
  • 项目实践:在仓库层封装关联操作(如 Repo.FindUserWithOrders())。测试关联迁移和查询。对于复杂项目,使用 GORM Gen 生成类型安全 DAO。
[up主专用,视频内嵌代码贴在这]