Gorm(八)预加载方式
在 Gorm 中,Preload 和 Joins 用于处理关联查询,解决“N+1 查询问题”并高效加载关联数据。二者适用场景不同,且支持灵活的自定义逻辑。以下是详细说明:
一、Preload:预加载关联数据(单次查询关联表)
Preload 会在主查询之后,单独执行一条 SQL 查询关联表,将关联数据一次性加载到主模型中,避免循环查询(N+1 问题)。适用于所有关联关系(一对一、一对多、多对多)。
1. 基础用法(单次预加载)
加载主模型时,同时加载指定的关联字段。
示例(一对多:查询用户时预加载订单):
type User struct {gorm.ModelName stringOrders []Order // 一对多关联
}type Order struct {gorm.ModelProduct stringUserID uint
}// 预加载用户的 Orders 关联
var users []User
db.Preload("Orders").Find(&users)
// 执行两条 SQL:
// 1. SELECT * FROM users;(查询主表)
// 2. SELECT * FROM orders WHERE user_id IN (1,2,3...);(查询关联表,IN 条件包含主表所有 ID)
结果:users 切片中每个 User 的 Orders 字段已填充对应的订单数据。
2. Preload 带条件(Preload(db.Where...))
预加载时可通过 db.Where 过滤关联数据,只加载符合条件的关联记录。
示例(只预加载金额大于 100 的订单):
var users []User
// 预加载用户的 Orders,且订单金额 > 100
db.Preload("Orders", "amount > ?", 100).Find(&users)
// 关联查询 SQL:SELECT * FROM orders WHERE user_id IN (...) AND amount > 100;
复杂条件:支持多个条件组合(使用 db.Where 链式调用):
db.Preload("Orders", func(tx *gorm.DB) *gorm.DB {return tx.Where("amount > 100").Where("status = ?", "paid")
}).Find(&users)
// 关联查询条件:amount > 100 AND status = 'paid'
3. 自定义 Preload(嵌套预加载、多级关联)
Preload 支持嵌套预加载(关联的关联),或通过自定义函数实现复杂逻辑。
示例 1:嵌套预加载(用户 → 订单 → 订单项):
type Order struct {gorm.ModelProduct stringUserID uintItems []OrderItem // 订单包含多个订单项(一对多)
}type OrderItem struct {gorm.ModelOrderID uintName string
}// 预加载用户的订单,同时预加载订单的订单项
db.Preload("Orders.Items").Find(&users)
// 执行 3 条 SQL:users → orders → order_items
示例 2:自定义预加载函数(排序、分页):
// 预加载用户的订单,并按创建时间倒序,只取前 5 条
db.Preload("Orders", func(tx *gorm.DB) *gorm.DB {return tx.Order("created_at DESC").Limit(5)
}).Find(&users)
二、Joins:关联查询(单次 SQL 联表查询)
Joins 通过 SQL 联表查询(LEFT JOIN / INNER JOIN) 将主表和关联表合并为一条 SQL,适用于一对一或需要过滤主表数据的场景(一对多联表会导致主表记录重复)。
1. 基础用法(单次联表查询)
示例(一对一:查询用户时联表查询身份证信息):
type User struct {gorm.ModelName stringIDCard IDCard // 一对一关联
}type IDCard struct {gorm.ModelNumber stringUserID uint
}// 联表查询用户和身份证(LEFT JOIN)
var users []User
db.Joins("IDCard").Find(&users)
// SQL:SELECT users.*, id_cards.* FROM users LEFT JOIN id_cards ON users.id = id_cards.user_id;
结果:User.IDCard 字段会填充关联的身份证数据。
2. Joins 带条件(过滤主表或关联表)
联表时可通过 Where 同时过滤主表和关联表的数据。
示例(查询有身份证且年龄 > 18 的用户):
db.Joins("IDCard").Where("users.age > ? AND id_cards.number IS NOT NULL", 18).Find(&users)
// SQL:LEFT JOIN 后通过 WHERE 过滤主表(users.age)和关联表(id_cards.number)
3. Joins 与 Select 结合(指定返回字段)
联表查询时可通过 Select 只返回需要的字段,避免字段冲突(如主表和关联表都有 created_at 时)。
db.Joins("IDCard").Select("users.id, users.name, id_cards.number").Find(&users)
// 只返回用户的 id、name 和身份证的 number 字段
三、Preload 与 Joins 的对比与适用场景
| 特性 | Preload | Joins |
|---|---|---|
| 底层实现 | 执行多条 SQL(主表 + 关联表) | 执行单条联表 SQL(JOIN) |
| 适用关联类型 | 一对一、一对多、多对多(均适用) | 优先一对一(一对多会导致主表记录重复) |
| 主表记录是否重复 | 不重复(关联数据放在切片/对象中) | 一对多场景会重复(主表记录随关联数复制) |
| 能否过滤主表数据 | 不能(仅过滤关联数据) | 能(通过联表条件过滤主表) |
| 性能 | 多表查询但避免重复数据,适合大量关联 | 单条 SQL 但可能返回重复数据,适合简单关联 |
四、最佳实践
-
一对多/多对多关联:优先用
Preload,避免Joins导致的主表记录重复。
例如:查询用户及其所有订单,Preload("Orders")比Joins("Orders")更清晰(后者会让一个用户对应多条重复记录)。 -
一对一关联:
Preload和Joins均可,Joins性能略优(单条 SQL)。
例如:查询用户及其身份证,Joins("IDCard")更高效。 -
需要通过关联表过滤主表:必须用
Joins。
例如:查询“有未支付订单的用户”,需通过Joins("Orders").Where("orders.status = 'unpaid'")过滤。 -
复杂关联逻辑:用
Preload自定义函数,支持排序、分页、嵌套预加载。
总结
Preload:通过多条 SQL 预加载关联数据,适合所有关联类型,避免主表重复,支持嵌套和条件过滤。Joins:通过单条联表 SQL 查询,适合一对一或需过滤主表的场景,注意一对多的记录重复问题。
根据关联类型和查询需求选择合适的方法,可兼顾性能和代码清晰度。
