Table of contents
Open Table of contents
TL;DR
GORM 支持四种关联类型(Belongs To / Has One / Has Many / Many To Many)和多态关联。默认是懒加载(不自动加载关联),用 Preload 做 eager loading(2 条 SQL 解决 N+1),用 Joins 做单条 JOIN 查询(适合按关联字段过滤)。Association Mode 提供 Find/Append/Replace/Delete/Clear/Count 六种操作管理关联关系。
一、四种关联类型
1. Belongs To(属于)
外键在当前模型上。每个 User 属于一个 Company:
type User struct {
gorm.Model
Name string
CompanyID int // 外键(约定:关联类型名 + 主键名)
Company Company // 关联字段
}
type Company struct {
ID int
Name string
}
外键约定:CompanyID = 关联类型名 Company + 主键名 ID。
自定义外键:
type User struct {
gorm.Model
Name string
CompanyRefer int
Company Company `gorm:"foreignKey:CompanyRefer"` // 用 CompanyRefer 而不是 CompanyID
}
自定义引用字段(不引用主键,引用其他字段):
type User struct {
gorm.Model
Name string
CompanyID string
Company Company `gorm:"references:Code"` // 引用 Company.Code 而不是 Company.ID
}
type Company struct {
ID int
Code string
Name string
}
外键约束(迁移时生效):
Company Company `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
2. Has One(拥有一个)
外键在关联模型上。每个 User 有一张 CreditCard:
type User struct {
gorm.Model
CreditCard CreditCard // 关联字段
}
type CreditCard struct {
gorm.Model
Number string
UserID uint // 外键(约定:拥有者类型名 + 主键名)
}
与 Belongs To 的区别:外键的位置不同。Belongs To 的外键在声明关联的模型上(User.CompanyID),Has One 的外键在被关联的模型上(CreditCard.UserID)。
自引用:
type User struct {
gorm.Model
Name string
ManagerID *uint
Manager *User
}
3. Has Many(拥有多个)
外键在关联模型上,一对多。每个 User 有多张 CreditCard:
type User struct {
gorm.Model
CreditCards []CreditCard // 切片 = Has Many
}
type CreditCard struct {
gorm.Model
Number string
UserID uint // 外键
}
自定义外键:
type User struct {
gorm.Model
CreditCards []CreditCard `gorm:"foreignKey:UserRefer"`
}
type CreditCard struct {
gorm.Model
Number string
UserRefer uint // 自定义外键名
}
自定义引用字段:
type User struct {
gorm.Model
MemberNumber string
CreditCards []CreditCard `gorm:"foreignKey:UserNumber;references:MemberNumber"`
}
type CreditCard struct {
gorm.Model
Number string
UserNumber string // 引用 User.MemberNumber
}
自引用(树形结构):
type User struct {
gorm.Model
Name string
ManagerID *uint
Team []User `gorm:"foreignkey:ManagerID"` // 一个 manager 有多个下属
}
4. Many To Many(多对多)
通过中间表关联。一个 User 可以说多种 Language,一种 Language 可以被多个 User 说:
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"` // 指定中间表名
}
type Language struct {
gorm.Model
Name string
Users []*User `gorm:"many2many:user_languages;"` // 反向引用(可选)
}
AutoMigrate 会自动创建 user_languages 中间表,包含 user_id 和 language_id 两个外键。
自引用多对多(好友关系):
type User struct {
gorm.Model
Friends []*User `gorm:"many2many:user_friends"`
}
// 中间表 user_friends 包含 user_id 和 friend_id
自定义中间表(需要在中间表上加额外字段时):
type PersonAddress struct {
PersonID int `gorm:"primaryKey"`
AddressID int `gorm:"primaryKey"`
CreatedAt time.Time
DeletedAt gorm.DeletedAt
}
// 在查询前设置
err := db.SetupJoinTable(&Person{}, "Addresses", &PersonAddress{})
自定义外键和引用:
type User struct {
gorm.Model
Profiles []Profile `gorm:"many2many:user_profiles;foreignKey:Refer;joinForeignKey:UserReferID;References:UserRefer;joinReferences:ProfileRefer"`
Refer uint `gorm:"index:,unique"`
}
四个 tag 的含义:
foreignKey:当前模型中作为外键源的字段joinForeignKey:中间表中引用当前模型的列名references:关联模型中作为引用的字段joinReferences:中间表中引用关联模型的列名
四种关联的判断口诀
| 关系 | 外键在哪 | 数量 | 典型场景 |
|---|---|---|---|
| Belongs To | 当前模型 | 一对一 | User 属于 Company |
| Has One | 关联模型 | 一对一 | User 有一张 CreditCard |
| Has Many | 关联模型 | 一对多 | User 有多张 CreditCard |
| Many To Many | 中间表 | 多对多 | User 说多种 Language |
关键区分:Belongs To vs Has One 的唯一区别是外键的归属。外键在谁身上,谁就是 “belongs to” 的那一方。
二、多态关联(Polymorphic)
多态让不同类型的模型共享同一个关联表。GORM 通过 OwnerType 字段区分所属类型。仅支持 Has One 和 Has Many。
基础用法
type Dog struct {
ID int
Name string
Toys []Toy `gorm:"polymorphic:Owner;"` // 声明多态,前缀为 Owner
}
type Cat struct {
ID int
Name string
Toy Toy `gorm:"polymorphic:Owner;"` // Has One 也支持
}
type Toy struct {
ID int
Name string
OwnerID int // 多态外键(Owner + ID)
OwnerType string // 多态类型字段(Owner + Type),值为表名如 "dogs"/"cats"
}
生成的 SQL:
-- 创建 Dog 和它的 Toys
INSERT INTO `dogs` (`name`) VALUES ("dog1")
INSERT INTO `toys` (`name`,`owner_id`,`owner_type`) VALUES
("toy1", 1, "dogs"),
("toy2", 1, "dogs")
自定义多态标签
type Dog struct {
ID int
Name string
Toys []Toy `gorm:"polymorphicType:Kind;polymorphicId:OwnerID;polymorphicValue:master"`
}
type Toy struct {
ID int
Name string
OwnerID int
Kind string // 类型字段改为 Kind
}
polymorphicType:自定义类型字段名(默认{前缀}Type)polymorphicId:自定义 ID 字段名(默认{前缀}ID)polymorphicValue:自定义类型值(默认表名)
INSERT INTO `toys` (`name`,`owner_id`,`kind`) VALUES
("toy1", 1, "master"), -- kind 存的是 "master" 而非 "dogs"
("toy2", 1, "master")
何时用多态
适用:多种不同类型的模型需要相同的关联结构(评论系统——文章、视频、帖子都能有评论)。
不适用:关联结构差异大、需要外键约束(多态无法建数据库级外键约束)、查询性能敏感(多一个 WHERE 条件过滤类型)。
三、Association Mode(关联模式)
Association Mode 提供了操作关联关系的统一接口,适用于所有关联类型。
var user User
db.First(&user, 1)
// 进入 Association Mode
assoc := db.Model(&user).Association("Languages")
if assoc.Error != nil {
// 处理错误
}
六种操作
// 1. Find — 查找关联
var languages []Language
db.Model(&user).Association("Languages").Find(&languages)
// 带条件查找
codes := []string{"zh-CN", "en-US", "ja-JP"}
db.Model(&user).Where("code IN ?", codes).Association("Languages").Find(&languages)
// 2. Append — 添加关联
// many2many / has many:追加新关联
db.Model(&user).Association("Languages").Append([]Language{languageZH, languageEN})
// belongs to / has one:替换当前关联
db.Model(&user).Association("CreditCard").Append(&CreditCard{Number: "411111111111"})
// 3. Replace — 替换所有关联
db.Model(&user).Association("Languages").Replace([]Language{languageZH, languageEN})
// 4. Delete — 删除关联(仅删关系,不删记录;将外键设为 NULL)
db.Model(&user).Association("Languages").Delete(languageZH, languageEN)
// 5. Clear — 清空所有关联
db.Model(&user).Association("Languages").Clear()
// 6. Count — 计数
count := db.Model(&user).Association("Languages").Count()
批量操作
var users = []User{user1, user2, user3}
// 批量查找
db.Model(&users).Association("Role").Find(&roles)
// 批量追加(每个 user 分别追加对应的关联)
db.Model(&users).Association("Team").Append(&userA, &userB, &[]User{userA, userB, userC})
// 批量替换
db.Model(&users).Association("Team").Replace(&userA, &userB, &[]User{userA, userB, userC})
// 批量计数
db.Model(&users).Association("Team").Count()
真正删除关联记录
默认行为:Replace/Delete/Clear 只是把外键设为 NULL,不删除记录。用 Unscoped() 真正删除:
// 删除关联关系并物理删除记录
db.Unscoped().Model(&user).Association("Languages").Unscoped().Clear()
Association Tags 速查
| Tag | 作用 |
|---|---|
foreignKey | 当前模型中作为外键的字段 |
references | 关联模型中被引用的字段 |
polymorphic | 声明多态,指定前缀 |
polymorphicValue | 自定义多态类型值(默认表名) |
many2many | 指定多对多中间表名 |
joinForeignKey | 中间表中引用当前模型的列 |
joinReferences | 中间表中引用关联模型的列 |
constraint | 外键约束(OnUpdate:CASCADE,OnDelete:SET NULL) |
四、Preloading(预加载)——核心话题
N+1 问题:怎么产生的
GORM 默认懒加载——查询 User 不会自动加载关联的 Orders。如果你循环访问每个 User 的 Orders:
var users []User
db.Find(&users) // 1 条 SQL:SELECT * FROM users
for _, u := range users {
db.Model(&u).Association("Orders").Find(&u.Orders) // 每个 user 1 条 SQL
}
// 10 个用户 = 1 + 10 = 11 条 SQL(N+1)
Preload 如何解决 N+1
var users []User
db.Preload("Orders").Find(&users)
// SQL 1: SELECT * FROM users;
// SQL 2: SELECT * FROM orders WHERE user_id IN (1,2,3,...);
// 总共 2 条 SQL,无论多少用户
内部执行流程(8 步):
Preload("Orders")把关联名存入Statement.Preloadsmap- 主查询执行,获取所有 User
- Preload 回调触发
- 从 User 结果中提取所有外键值(UserID),构建 Identity Map(
{"1": &User{ID:1}, "2": ...}) - 用
IN子句查询关联表:SELECT * FROM orders WHERE user_id IN (1,2,3,...) - 用 Identity Map 做 O(1) 匹配,把 Order 分配到对应的 User
- 设置 User 的 Orders 字段
性能对比:
| 场景 | 不用 Preload | 用 Preload | 优化幅度 |
|---|---|---|---|
| 100 个 User,各 5 个 Order | 101 条 SQL | 2 条 SQL | 98% |
| 嵌套:100 User → Orders → OrderItems | 10,101 条 SQL | 3 条 SQL | 99.97% |
多个关联的 Preload
db.Preload("Orders").Preload("Profile").Preload("Role").Find(&users)
// SQL 1: SELECT * FROM users;
// SQL 2: SELECT * FROM orders WHERE user_id IN (...); -- has many
// SQL 3: SELECT * FROM profiles WHERE user_id IN (...); -- has one
// SQL 4: SELECT * FROM roles WHERE id IN (...); -- belongs to
嵌套 Preload(用点号表示)
// 加载 User → Orders → OrderItems → Product
db.Preload("Orders.OrderItems.Product").Preload("CreditCard").Find(&users)
// N 层嵌套 = N+1 条 SQL(线性增长,不是指数增长)
嵌套带条件:
db.Preload("Orders", "state = ?", "paid").Preload("Orders.OrderItems").Find(&users)
// 只预加载 state="paid" 的 Orders,以及这些 Orders 的 OrderItems
条件 Preload
// 方式一:直接传条件
db.Preload("Orders", "state NOT IN (?)", "cancelled").Find(&users)
// 方式二:传闭包(更灵活)
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Order("orders.amount DESC")
}).Find(&users)
// 与主查询条件组合
db.Where("state = ?", "active").
Preload("Orders", "state NOT IN (?)", "cancelled").
Find(&users)
Preload All
// 预加载所有一级关联
db.Preload(clause.Associations).Find(&users)
// 先指定嵌套,再 Preload All(嵌套不会被 Associations 覆盖)
db.Preload("Orders.OrderItems.Product").Preload(clause.Associations).Find(&users)
注意:clause.Associations 只加载一级关联,不会递归加载嵌套关联。
Joins Preloading(JOIN 预加载)
用 LEFT JOIN 代替独立查询,一条 SQL 搞定。只适合一对一关系(Has One / Belongs To):
db.Joins("Company").Find(&users)
// SQL: SELECT users.*, Company.* FROM users LEFT JOIN companies AS Company ON ...
// 带条件的 Joins
db.Joins("Company", db.Where(&Company{Alive: true})).Find(&users)
// 嵌套 Joins
db.Joins("Manager").Joins("Manager.Company").Find(&users)
Preload vs Joins:什么时候用哪个
| 维度 | Preload | Joins |
|---|---|---|
| SQL 数量 | 多条(每个关联一条) | 1 条 |
| 适用关系 | 所有(Has Many, Many2Many 尤其适合) | 只适合一对一(Has One, Belongs To) |
| 结果集重复 | 无重复 | Has Many 会导致父记录重复行 |
| 按关联字段过滤主模型 | 不行 | 可以(WHERE 条件) |
| 匹配方式 | 内存 Identity Map(O(1)) | 数据库 JOIN |
| 典型场景 | 加载关联数据用于展示 | 按关联条件筛选主记录 |
核心判断:
- 要加载关联数据 →
Preload - 要按关联字段过滤主模型 →
Joins - Has Many / Many2Many → 只用
Preload(JOIN 会产生笛卡尔积) - 混合使用:
Joins过滤 +Preload加载
// 典型混合用法:找有活跃公司的用户,同时预加载 Orders
db.Joins("Company", db.Where(&Company{Alive: true})).
Preload("Orders").
Find(&users)
Many2Many Preload 的内部细节
Many2Many 的 Preload 比一般关联多一步——需要查中间表:
- 查中间表:
SELECT * FROM user_languages WHERE user_id IN (...) - 查关联表:
SELECT * FROM languages WHERE id IN (中间表中的 language_id) - 通过转换后的 Identity Map 把 Language 映射回 User
所以 Many2Many 的 Preload 实际是 3 条 SQL(主表 + 中间表 + 关联表)。
内存注意事项
- Preload 在内存中构建 Identity Map,10,000+ 记录时考虑分页
- 嵌套 Preload 会乘数级增加内存使用
- Identity Map 在 Preload 完成后释放
- 空关联会被初始化为空 slice(
[]),不是nil——JSON 序列化友好
五、高级查询技巧
Scopes(可复用查询条件)
Scope 是 func(*gorm.DB) *gorm.DB 签名的函数,用于封装可复用的查询逻辑:
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("amount > ?", 1000)
}
func PaidWithCreditCard(db *gorm.DB) *gorm.DB {
return db.Where("pay_mode_sign = ?", "C")
}
// 带参数的 Scope(返回闭包)
func OrderStatus(status []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("status IN (?)", status)
}
}
// 使用
db.Scopes(AmountGreaterThan1000, PaidWithCreditCard).Find(&orders)
db.Scopes(AmountGreaterThan1000, OrderStatus([]string{"paid", "shipped"})).Find(&orders)
Scope 的价值:把业务查询逻辑从 handler 层抽出来,Scope 可以单元测试、可组合、可复用。
SubQuery(子查询)
把 *gorm.DB 对象作为参数传入,GORM 自动生成子查询:
// 金额大于平均值的订单
db.Where("amount > (?)", db.Table("orders").Select("AVG(amount)")).Find(&orders)
// SELECT * FROM orders WHERE amount > (SELECT AVG(amount) FROM orders);
// FROM 子查询
db.Table("(?) as u", db.Model(&User{}).Select("name", "age")).
Where("age = ?", 18).Find(&User{})
// SELECT * FROM (SELECT `name`,`age` FROM `users`) as u WHERE `age` = 18
// 多个 FROM 子查询
subQuery1 := db.Model(&User{}).Select("name")
subQuery2 := db.Model(&Pet{}).Select("name")
db.Table("(?) as u, (?) as p", subQuery1, subQuery2).Find(&User{})
Named Arguments(命名参数)
// 用 sql.Named
db.Where("name1 = @name OR name2 = @name", sql.Named("name", "jinzhu")).Find(&user)
// 用 map
db.Where("name1 = @name OR name2 = @name", map[string]interface{}{"name": "jinzhu"}).First(&user)
// Raw SQL 也支持
db.Raw("SELECT * FROM users WHERE name1 = @name OR name2 = @name2",
sql.Named("name", "jinzhu1"), sql.Named("name2", "jinzhu2")).Find(&user)
// 用 struct
type NamedArgument struct {
Name string
Name2 string
}
db.Raw("SELECT * FROM users WHERE name1 = @Name AND name2 = @Name2",
NamedArgument{Name: "jinzhu", Name2: "jinzhu2"}).Find(&user)
Smart Select(智能字段选择)
查询结果扫描到字段更少的结构体时,GORM 自动只 SELECT 目标结构体有的字段:
type User struct {
ID uint
Name string
Age int
Gender string
}
type APIUser struct {
ID uint
Name string
}
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SQL: SELECT `id`, `name` FROM `users` LIMIT 10
不需要手动写 Select("id", "name"),减少数据传输。
Raw SQL 和 Exec
当 GORM 的链式 API 无法表达复杂查询时,用 Raw SQL:
// 查询
var result Result
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)
// 聚合
var age int
db.Raw("SELECT SUM(age) FROM users WHERE role = ?", "admin").Scan(&age)
// 执行(不返回数据)
db.Exec("UPDATE orders SET shipped_at = ? WHERE id IN ?", time.Now(), []int64{1, 2, 3})
// 用 gorm.Expr 嵌入表达式
db.Exec("UPDATE users SET money = ? WHERE name = ?",
gorm.Expr("money * ? + ?", 10000, 1), "jinzhu")
DryRun 模式(生成 SQL 不执行)
调试利器——看 GORM 生成了什么 SQL:
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() // => SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
stmt.Vars // => []interface{}{1}
ToSQL 更简洁:
sql := db.ToSQL(func(tx *gorm.DB) *gorm.DB {
return tx.Model(&User{}).Where("id = ?", 100).Limit(10).Order("age desc").Find(&[]User{})
})
// => SELECT * FROM "users" WHERE id = 100 AND "users"."deleted_at" IS NULL ORDER BY age desc LIMIT 10
注意:ToSQL 生成的 SQL 不提供安全保证(参数没有转义),仅用于调试。
六、性能优化要点
// 1. 禁用默认事务(读操作密集时提升显著)
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
// 2. 预编译语句缓存
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
PrepareStmt: true,
})
// 3. Index Hints
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
七、常见误区(Pitfalls)
-
Joins 加载 Has Many 导致重复行:JOIN 一对多时父记录会重复。如果只是要加载关联数据,用 Preload,不要用 Joins。
-
clause.Associations不递归:Preload(clause.Associations)只加载第一层关联。嵌套关联必须用点号显式声明Preload("Orders.OrderItems")。 -
Preload 条件不会过滤主模型:
db.Preload("Orders", "state = ?", "paid").Find(&users)返回所有 User,只是每个 User 的 Orders 中只包含 paid 的。如果要只返回有 paid order 的 User,用 Joins。 -
忘记 SetupJoinTable:自定义中间表模型必须在查询前调用
db.SetupJoinTable(),否则 GORM 不知道用你的自定义结构。 -
多态无法建数据库外键约束:
OwnerType+OwnerID是应用层约束,数据库层没有 FK。数据一致性完全靠应用保证。 -
Association Mode 要求主键非零:
db.Select("Account").Delete(&User{})不会删除关联,因为 User 的主键是零值。必须&User{ID: 1}。 -
Preload 内存消耗:大量数据(10,000+)的 Preload 会在内存中构建 Identity Map。生产环境务必分页。