预加载

预加载是解决 N+1 查询问题的利器。理解它的工作原理,能显著提升查询性能。

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:

  • 1 条查用户
  • 10 条查每个用户的订单

用户越多,查询越多,性能越差。

Preload 预加载

用 Preload 一次加载所有关联:

var users []User
db.Preload("Orders").Find(&users)

只执行 2 条 SQL:

  • 1 条查用户
  • 1 条查所有用户的订单

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)

Joins 预加载

用 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 条订单。

解决方案:条件预加载或分批加载。

内存占用

预加载把所有数据加载到内存,注意内存限制。

查询次数

预加载减少查询次数,但单次查询数据量更大。权衡选择。

预加载 vs 延迟加载

预加载

  • 提前加载关联
  • 减少查询次数
  • 适合确定会用到的关联

延迟加载

  • 按需加载
  • 查询次数多
  • 适合可能不用的关联

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 的区别,根据场景选择合适的策略。注意数据量和内存占用,避免过度预加载。