预加载是解决 N+1 查询问题的利器。理解它的工作原理,能显著提升查询性能。
什么是 N+1 问题?
var users []User
db.Find(&users)
for _, user := range users {
var orders []Order
db.Where("user_id = ?", user.ID).Find(&orders)
user.Orders = orders
}
查询 10 个用户,会执行 11 条 SQL:
用户越多,查询越多,性能越差。
用 Preload 一次加载所有关联:
var users []User
db.Preload("Orders").Find(&users)
只执行 2 条 SQL:
GORM 自动关联,每个用户的 Orders 字段都有值。
加载多层关联:
type User struct {
ID uint
Orders []Order
}
type Order struct {
ID uint
UserID uint
Items []OrderItem
}
type OrderItem struct {
ID uint
OrderID uint
}
db.Preload("Orders.Items").Find(&users)
同时加载多个关联:
type User struct {
ID uint
Orders []Order
Profile Profile
}
db.Preload("Orders").Preload("Profile").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")
}).Find(&users)
用 Join 方式预加载:
db.Joins("Orders").Find(&users)
只执行 1 条 SQL,但结果可能重复。适合一对一关联。
Preload(默认)
Joins
db.Preload(clause.Associations).Find(&users)
加载所有定义的关联。
复杂场景自定义预加载逻辑:
var users []User
db.Find(&users)
userIDs := make([]uint, len(users))
for i, u := range users {
userIDs[i] = u.ID
}
var orders []Order
db.Where("user_id IN ?", userIDs).Find(&orders)
orderMap := make(map[uint][]Order)
for _, o := range orders {
orderMap[o.UserID] = append(orderMap[o.UserID], o)
}
for i := range users {
users[i].Orders = orderMap[users[i].ID]
}
手动实现,更灵活控制。
数据量
关联数据量大时,预加载会加载全部数据:
db.Preload("Orders").Find(&users)
如果每个用户有 1000 个订单,10 个用户就是 10000 条订单。
解决方案:条件预加载或分批加载。
内存占用
预加载把所有数据加载到内存,注意内存限制。
查询次数
预加载减少查询次数,但单次查询数据量更大。权衡选择。
预加载
延迟加载
GORM 默认不延迟加载,可以手动实现:
func (u *User) GetOrders() []Order {
if u.orders == nil {
db.Where("user_id = ?", u.ID).Find(&u.orders)
}
return u.orders
}
博客文章与评论
type Post struct {
ID uint
Title string
Comments []Comment
}
var posts []Post
db.Preload("Comments", "status = ?", "approved").Find(&posts)
只加载已审核的评论。
用户与角色权限
type User struct {
ID uint
Roles []Role `gorm:"many2many:user_roles;"`
}
type Role struct {
ID uint
Permissions []Permission `gorm:"many2many:role_permissions;"`
}
db.Preload("Roles.Permissions").Find(&users)
嵌套预加载多对多关联。
订单与商品
type Order struct {
ID uint
Items []OrderItem
}
type OrderItem struct {
ID uint
OrderID uint
ProductID uint
Product Product
}
db.Preload("Items.Product").Find(&orders)
加载订单项及其商品信息。
预加载是解决 N+1 问题的标准方案。理解 Preload 和 Joins 的区别,根据场景选择合适的策略。注意数据量和内存占用,避免过度预加载。